fixed renderers and progress tracking for generation part
This commit is contained in:
parent
bc2dd6687d
commit
911bcffcd7
5 changed files with 1905 additions and 757 deletions
|
|
@ -114,15 +114,28 @@ class StructureFiller:
|
|||
Phase 5D.1: Generiert Sections-Struktur für jedes Chapter (ohne Content).
|
||||
Sections enthalten: content_type, contentPartIds, generationHint, useAiCall
|
||||
"""
|
||||
# Count total chapters for progress tracking
|
||||
totalChapters = sum(len(doc.get("chapters", [])) for doc in chapterStructure.get("documents", []))
|
||||
chapterIndex = 0
|
||||
|
||||
for doc in chapterStructure.get("documents", []):
|
||||
for chapter in doc.get("chapters", []):
|
||||
chapterIndex += 1
|
||||
chapterId = chapter.get("id", "unknown")
|
||||
chapterLevel = chapter.get("level", 1)
|
||||
chapterTitle = chapter.get("title", "")
|
||||
chapterTitle = chapter.get("title", "Untitled Chapter")
|
||||
generationHint = chapter.get("generationHint", "")
|
||||
contentPartIds = chapter.get("contentPartIds", [])
|
||||
contentPartInstructions = chapter.get("contentPartInstructions", {})
|
||||
|
||||
# Update progress for chapter structure generation
|
||||
progress = chapterIndex / totalChapters if totalChapters > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
parentOperationId,
|
||||
progress,
|
||||
f"Generating sections for Chapter {chapterIndex}/{totalChapters}: {chapterTitle}"
|
||||
)
|
||||
|
||||
chapterPrompt = self._buildChapterSectionsStructurePrompt(
|
||||
chapterId=chapterId,
|
||||
chapterLevel=chapterLevel,
|
||||
|
|
@ -194,19 +207,55 @@ class StructureFiller:
|
|||
"""
|
||||
Phase 5D.2: Füllt Sections mit ContentParts.
|
||||
"""
|
||||
# Sammle alle Sections für sequenzielle Verarbeitung
|
||||
sections_to_process = []
|
||||
all_sections_list = [] # Für Kontext-Informationen
|
||||
# Sammle alle Sections für Kontext-Informationen (für alle Sections)
|
||||
all_sections_list = []
|
||||
for doc in chapterStructure.get("documents", []):
|
||||
for chapter in doc.get("chapters", []):
|
||||
for section in chapter.get("sections", []):
|
||||
all_sections_list.append(section)
|
||||
sections_to_process.append((doc, chapter, section))
|
||||
|
||||
# Sequenzielle Section-Generierung
|
||||
# Berechne Gesamtanzahl Chapters für Progress-Tracking
|
||||
totalChapters = sum(len(doc.get("chapters", [])) for doc in chapterStructure.get("documents", []))
|
||||
fillOperationId = parentOperationId
|
||||
for sectionIndex, (doc, chapter, section) in enumerate(sections_to_process):
|
||||
|
||||
# Helper function to calculate overall progress
|
||||
def calculateOverallProgress(chapterIndex, totalChapters, sectionIndex, totalSections):
|
||||
"""Calculate overall progress: 0.0 to 1.0"""
|
||||
if totalChapters == 0:
|
||||
return 1.0
|
||||
|
||||
# Progress from completed chapters (0 to chapterIndex-1)
|
||||
completedChaptersProgress = chapterIndex / totalChapters
|
||||
|
||||
# Progress from current chapter (sectionIndex / totalSections)
|
||||
currentChapterProgress = (sectionIndex / totalSections) / totalChapters if totalSections > 0 else 0
|
||||
|
||||
return min(1.0, completedChaptersProgress + currentChapterProgress)
|
||||
|
||||
# Process chapters sequentially with chapter-level progress
|
||||
chapterIndex = 0
|
||||
for doc in chapterStructure.get("documents", []):
|
||||
for chapter in doc.get("chapters", []):
|
||||
chapterIndex += 1
|
||||
chapterId = chapter.get("id", "unknown")
|
||||
chapterTitle = chapter.get("title", "Untitled Chapter")
|
||||
sections = chapter.get("sections", [])
|
||||
totalSections = len(sections)
|
||||
|
||||
# Start chapter operation
|
||||
chapterOperationId = f"{fillOperationId}_chapter_{chapterId}"
|
||||
self.services.chat.progressLogStart(
|
||||
chapterOperationId,
|
||||
"Chapter Generation",
|
||||
f"Chapter {chapterIndex}/{totalChapters}",
|
||||
chapterTitle,
|
||||
parentOperationId=fillOperationId
|
||||
)
|
||||
|
||||
# Process sections within chapter
|
||||
for sectionIndex, section in enumerate(sections):
|
||||
sectionId = section.get("id")
|
||||
sectionTitle = section.get("title", sectionId)
|
||||
contentPartIds = section.get("contentPartIds", [])
|
||||
contentFormats = section.get("contentFormats", {})
|
||||
# Check both camelCase and snake_case for generationHint
|
||||
|
|
@ -214,6 +263,14 @@ class StructureFiller:
|
|||
contentType = section.get("content_type", "paragraph")
|
||||
useAiCall = section.get("useAiCall", False)
|
||||
|
||||
# Update overall progress at start of section
|
||||
overallProgress = calculateOverallProgress(chapterIndex - 1, totalChapters, sectionIndex, totalSections)
|
||||
self.services.chat.progressLogUpdate(
|
||||
fillOperationId,
|
||||
overallProgress,
|
||||
f"Chapter {chapterIndex}/{totalChapters}, Section {sectionIndex + 1}/{totalSections}: {sectionTitle}"
|
||||
)
|
||||
|
||||
# WICHTIG: Wenn keine ContentParts vorhanden sind UND kein generationHint, kann kein AI-Call gemacht werden
|
||||
# Aber: Wenn generationHint vorhanden ist, SOLLTE AI verwendet werden, auch wenn useAiCall=false gesetzt ist
|
||||
# (z.B. wenn AI die Struktur generiert hat, aber useAiCall falsch gesetzt wurde)
|
||||
|
|
@ -302,22 +359,28 @@ class StructureFiller:
|
|||
# Erstelle Operation-ID für Section-Generierung
|
||||
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
||||
|
||||
# Starte ChatLog mit Parent-Referenz
|
||||
# Starte ChatLog mit Parent-Referenz (chapter, not fillOperationId)
|
||||
self.services.chat.progressLogStart(
|
||||
sectionOperationId,
|
||||
"Section Generation (Aggregation)",
|
||||
"Section",
|
||||
f"Generating section {sectionId} with {len(extractedParts)} parts",
|
||||
parentOperationId=fillOperationId
|
||||
f"Section {sectionIndex + 1}/{totalSections}",
|
||||
f"{sectionTitle} ({len(extractedParts)} parts)",
|
||||
parentOperationId=chapterOperationId
|
||||
)
|
||||
|
||||
try:
|
||||
# Update: Building prompt
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.2, "Building generation prompt")
|
||||
|
||||
# Debug: Log Prompt
|
||||
self.services.utils.writeDebugFile(
|
||||
generationPrompt,
|
||||
f"section_content_{sectionId}_prompt"
|
||||
f"{chapterId}_section_{sectionId}_prompt"
|
||||
)
|
||||
logger.debug(f"Logged section prompt: section_content_{sectionId}_prompt (aggregation)")
|
||||
logger.debug(f"Logged section prompt: {chapterId}_section_{sectionId}_prompt (aggregation)")
|
||||
|
||||
# Update: Calling AI
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.4, "Calling AI for content generation")
|
||||
|
||||
# Verwende callAi für ContentParts-Unterstützung (nicht callAiPlanning!)
|
||||
# Use IMAGE_GENERATE for image content type
|
||||
|
|
@ -344,29 +407,64 @@ class StructureFiller:
|
|||
)
|
||||
aiResponse = await self.aiService.callAi(request)
|
||||
|
||||
# Update: Processing response
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||
|
||||
# Debug: Log Response
|
||||
self.services.utils.writeDebugFile(
|
||||
aiResponse.content,
|
||||
f"section_content_{sectionId}_response"
|
||||
f"{chapterId}_section_{sectionId}_response"
|
||||
)
|
||||
logger.debug(f"Logged section response: section_content_{sectionId}_response (aggregation)")
|
||||
logger.debug(f"Logged section response: {chapterId}_section_{sectionId}_response (aggregation)")
|
||||
|
||||
# Update: Validating content
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.8, "Validating generated content")
|
||||
|
||||
# Handle IMAGE_GENERATE differently - returns image data directly
|
||||
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
||||
import base64
|
||||
base64Data = ""
|
||||
|
||||
# Convert image data to base64 string if needed
|
||||
if isinstance(aiResponse.content, bytes):
|
||||
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
||||
elif isinstance(aiResponse.content, str):
|
||||
# Check if it's already a JSON structure
|
||||
try:
|
||||
# Try to parse as JSON first
|
||||
jsonContent = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
|
||||
# If it's already a proper JSON structure with image element, use it
|
||||
if isinstance(jsonContent, dict) and jsonContent.get("type") == "image":
|
||||
elements.append(jsonContent)
|
||||
logger.debug("AI returned proper JSON image structure")
|
||||
continue
|
||||
elif isinstance(jsonContent, list) and len(jsonContent) > 0:
|
||||
# Check if first element is an image
|
||||
if isinstance(jsonContent[0], dict) and jsonContent[0].get("type") == "image":
|
||||
elements.extend(jsonContent)
|
||||
logger.debug("AI returned proper JSON image structure in list")
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
# Not JSON, treat as base64 string or data URI
|
||||
pass
|
||||
|
||||
# Already base64 string or data URI
|
||||
if aiResponse.content.startswith("data:image/"):
|
||||
# Extract base64 from data URI
|
||||
base64Data = aiResponse.content.split(",", 1)[1]
|
||||
else:
|
||||
# Check if it looks like base64 (alphanumeric + / + =)
|
||||
content_stripped = aiResponse.content.strip()
|
||||
if len(content_stripped) > 100 and all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r\t " for c in content_stripped[:200]):
|
||||
# Looks like base64, use it
|
||||
base64Data = content_stripped.replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "")
|
||||
else:
|
||||
base64Data = aiResponse.content
|
||||
else:
|
||||
base64Data = ""
|
||||
|
||||
# Always create proper JSON structure for images
|
||||
if base64Data:
|
||||
elements.append({
|
||||
"type": "image",
|
||||
"content": {
|
||||
|
|
@ -375,8 +473,17 @@ class StructureFiller:
|
|||
"caption": ""
|
||||
}
|
||||
})
|
||||
logger.debug(f"Created proper JSON image structure with base64Data length: {len(base64Data)}")
|
||||
else:
|
||||
logger.warning(f"IMAGE_GENERATE returned empty or invalid content for section {sectionId}")
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Image generation returned empty or invalid content",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
else:
|
||||
# Parse JSON response for other content types
|
||||
try:
|
||||
generatedElements = json.loads(
|
||||
self.services.utils.jsonExtractString(aiResponse.content)
|
||||
)
|
||||
|
|
@ -384,10 +491,39 @@ class StructureFiller:
|
|||
elements.extend(generatedElements)
|
||||
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
||||
elements.extend(generatedElements["elements"])
|
||||
elif isinstance(generatedElements, dict) and generatedElements.get("type"):
|
||||
# Single element in dict format
|
||||
elements.append(generatedElements)
|
||||
except (json.JSONDecodeError, ValueError) as json_error:
|
||||
logger.error(f"Error parsing JSON response for section {sectionId}: {str(json_error)}")
|
||||
# Try to extract any image data that might be in the response
|
||||
if contentType == "image":
|
||||
# Check if response content might be base64 image data
|
||||
content_str = str(aiResponse.content)
|
||||
if len(content_str) > 100:
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Failed to parse image generation response: {str(json_error)}",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
else:
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Failed to parse JSON response: {str(json_error)}",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
|
||||
# ChatLog abschließen
|
||||
self.services.chat.progressLogFinish(sectionOperationId, True)
|
||||
|
||||
# Update chapter progress after section completion
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
||||
self.services.chat.progressLogFinish(sectionOperationId, False)
|
||||
|
|
@ -397,6 +533,13 @@ class StructureFiller:
|
|||
"sectionId": sectionId
|
||||
})
|
||||
logger.error(f"Error generating section {sectionId}: {str(e)}")
|
||||
# Still update chapter progress even on error
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed (with errors)"
|
||||
)
|
||||
# NICHT raise - Section wird mit Fehlermeldung gerendert
|
||||
|
||||
else:
|
||||
|
|
@ -418,22 +561,28 @@ class StructureFiller:
|
|||
# Erstelle Operation-ID für Section-Generierung
|
||||
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
||||
|
||||
# Starte ChatLog mit Parent-Referenz
|
||||
# Starte ChatLog mit Parent-Referenz (chapter, not fillOperationId)
|
||||
self.services.chat.progressLogStart(
|
||||
sectionOperationId,
|
||||
"Section Generation",
|
||||
"Section",
|
||||
f"Generating section {sectionId} from generationHint",
|
||||
parentOperationId=fillOperationId
|
||||
f"Section {sectionIndex + 1}/{totalSections}",
|
||||
f"{sectionTitle} (from generationHint)",
|
||||
parentOperationId=chapterOperationId
|
||||
)
|
||||
|
||||
try:
|
||||
# Update: Building prompt
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.2, "Building generation prompt")
|
||||
|
||||
# Debug: Log Prompt
|
||||
self.services.utils.writeDebugFile(
|
||||
generationPrompt,
|
||||
f"section_content_{sectionId}_prompt"
|
||||
f"{chapterId}_section_{sectionId}_prompt"
|
||||
)
|
||||
logger.debug(f"Logged section prompt: section_content_{sectionId}_prompt")
|
||||
logger.debug(f"Logged section prompt: {chapterId}_section_{sectionId}_prompt")
|
||||
|
||||
# Update: Calling AI
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.4, "Calling AI for content generation")
|
||||
|
||||
# Verwende callAi ohne ContentParts
|
||||
operationType = OperationTypeEnum.IMAGE_GENERATE if contentType == "image" else OperationTypeEnum.DATA_ANALYSE
|
||||
|
|
@ -457,29 +606,57 @@ class StructureFiller:
|
|||
)
|
||||
aiResponse = await self.aiService.callAi(request)
|
||||
|
||||
# Update: Processing response
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||
|
||||
# Debug: Log Response
|
||||
self.services.utils.writeDebugFile(
|
||||
aiResponse.content,
|
||||
f"section_content_{sectionId}_response"
|
||||
f"{chapterId}_section_{sectionId}_response"
|
||||
)
|
||||
logger.debug(f"Logged section response: section_content_{sectionId}_response")
|
||||
logger.debug(f"Logged section response: {chapterId}_section_{sectionId}_response")
|
||||
|
||||
# Update: Validating content
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.8, "Validating generated content")
|
||||
|
||||
# Handle IMAGE_GENERATE differently - returns image data directly
|
||||
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
||||
import base64
|
||||
base64Data = ""
|
||||
|
||||
# Convert image data to base64 string if needed
|
||||
if isinstance(aiResponse.content, bytes):
|
||||
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
||||
elif isinstance(aiResponse.content, str):
|
||||
# Check if it's already a JSON structure
|
||||
try:
|
||||
jsonContent = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
|
||||
if isinstance(jsonContent, dict) and jsonContent.get("type") == "image":
|
||||
elements.append(jsonContent)
|
||||
logger.debug("AI returned proper JSON image structure")
|
||||
continue
|
||||
elif isinstance(jsonContent, list) and len(jsonContent) > 0:
|
||||
if isinstance(jsonContent[0], dict) and jsonContent[0].get("type") == "image":
|
||||
elements.extend(jsonContent)
|
||||
logger.debug("AI returned proper JSON image structure in list")
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
# Already base64 string or data URI
|
||||
if aiResponse.content.startswith("data:image/"):
|
||||
# Extract base64 from data URI
|
||||
base64Data = aiResponse.content.split(",", 1)[1]
|
||||
else:
|
||||
content_stripped = aiResponse.content.strip()
|
||||
if len(content_stripped) > 100 and all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r\t " for c in content_stripped[:200]):
|
||||
base64Data = content_stripped.replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "")
|
||||
else:
|
||||
base64Data = aiResponse.content
|
||||
else:
|
||||
base64Data = ""
|
||||
|
||||
# Always create proper JSON structure for images
|
||||
if base64Data:
|
||||
elements.append({
|
||||
"type": "image",
|
||||
"content": {
|
||||
|
|
@ -488,8 +665,17 @@ class StructureFiller:
|
|||
"caption": ""
|
||||
}
|
||||
})
|
||||
logger.debug(f"Created proper JSON image structure with base64Data length: {len(base64Data)}")
|
||||
else:
|
||||
logger.warning(f"IMAGE_GENERATE returned empty content for section {sectionId}")
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Image generation returned empty content",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
else:
|
||||
# Parse JSON response for other content types
|
||||
try:
|
||||
generatedElements = json.loads(
|
||||
self.services.utils.jsonExtractString(aiResponse.content)
|
||||
)
|
||||
|
|
@ -497,10 +683,27 @@ class StructureFiller:
|
|||
elements.extend(generatedElements)
|
||||
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
||||
elements.extend(generatedElements["elements"])
|
||||
elif isinstance(generatedElements, dict) and generatedElements.get("type"):
|
||||
elements.append(generatedElements)
|
||||
except (json.JSONDecodeError, ValueError) as json_error:
|
||||
logger.error(f"Error parsing JSON response for section {sectionId}: {str(json_error)}")
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Failed to parse JSON response: {str(json_error)}",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
|
||||
# ChatLog abschließen
|
||||
self.services.chat.progressLogFinish(sectionOperationId, True)
|
||||
|
||||
# Update chapter progress after section completion
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
||||
self.services.chat.progressLogFinish(sectionOperationId, False)
|
||||
|
|
@ -510,6 +713,13 @@ class StructureFiller:
|
|||
"sectionId": sectionId
|
||||
})
|
||||
logger.error(f"Error generating section {sectionId}: {str(e)}")
|
||||
# Still update chapter progress even on error
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed (with errors)"
|
||||
)
|
||||
|
||||
# Einzelverarbeitung: Jeder Part einzeln
|
||||
for partId in contentPartIds:
|
||||
|
|
@ -567,22 +777,28 @@ class StructureFiller:
|
|||
# Erstelle Operation-ID für Section-Generierung
|
||||
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
||||
|
||||
# Starte ChatLog mit Parent-Referenz
|
||||
# Starte ChatLog mit Parent-Referenz (chapter, not fillOperationId)
|
||||
self.services.chat.progressLogStart(
|
||||
sectionOperationId,
|
||||
"Section Generation",
|
||||
"Section",
|
||||
f"Generating section {sectionId}",
|
||||
parentOperationId=fillOperationId
|
||||
f"Section {sectionIndex + 1}/{totalSections}",
|
||||
f"{sectionTitle} (single part)",
|
||||
parentOperationId=chapterOperationId
|
||||
)
|
||||
|
||||
try:
|
||||
# Update: Building prompt
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.2, "Building generation prompt")
|
||||
|
||||
# Debug: Log Prompt
|
||||
self.services.utils.writeDebugFile(
|
||||
generationPrompt,
|
||||
f"section_content_{sectionId}_prompt"
|
||||
f"{chapterId}_section_{sectionId}_prompt"
|
||||
)
|
||||
logger.debug(f"Logged section prompt: section_content_{sectionId}_prompt")
|
||||
logger.debug(f"Logged section prompt: {chapterId}_section_{sectionId}_prompt")
|
||||
|
||||
# Update: Calling AI
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.4, "Calling AI for content generation")
|
||||
|
||||
# Verwende callAi für ContentParts-Unterstützung
|
||||
# Use IMAGE_GENERATE for image content type
|
||||
|
|
@ -609,29 +825,57 @@ class StructureFiller:
|
|||
)
|
||||
aiResponse = await self.aiService.callAi(request)
|
||||
|
||||
# Update: Processing response
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||
|
||||
# Debug: Log Response
|
||||
self.services.utils.writeDebugFile(
|
||||
aiResponse.content,
|
||||
f"section_content_{sectionId}_response"
|
||||
f"{chapterId}_section_{sectionId}_response"
|
||||
)
|
||||
logger.debug(f"Logged section response: section_content_{sectionId}_response")
|
||||
logger.debug(f"Logged section response: {chapterId}_section_{sectionId}_response")
|
||||
|
||||
# Update: Validating content
|
||||
self.services.chat.progressLogUpdate(sectionOperationId, 0.8, "Validating generated content")
|
||||
|
||||
# Handle IMAGE_GENERATE differently - returns image data directly
|
||||
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
||||
import base64
|
||||
base64Data = ""
|
||||
|
||||
# Convert image data to base64 string if needed
|
||||
if isinstance(aiResponse.content, bytes):
|
||||
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
||||
elif isinstance(aiResponse.content, str):
|
||||
# Check if it's already a JSON structure
|
||||
try:
|
||||
jsonContent = json.loads(self.services.utils.jsonExtractString(aiResponse.content))
|
||||
if isinstance(jsonContent, dict) and jsonContent.get("type") == "image":
|
||||
elements.append(jsonContent)
|
||||
logger.debug("AI returned proper JSON image structure")
|
||||
continue
|
||||
elif isinstance(jsonContent, list) and len(jsonContent) > 0:
|
||||
if isinstance(jsonContent[0], dict) and jsonContent[0].get("type") == "image":
|
||||
elements.extend(jsonContent)
|
||||
logger.debug("AI returned proper JSON image structure in list")
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
# Already base64 string or data URI
|
||||
if aiResponse.content.startswith("data:image/"):
|
||||
# Extract base64 from data URI
|
||||
base64Data = aiResponse.content.split(",", 1)[1]
|
||||
else:
|
||||
content_stripped = aiResponse.content.strip()
|
||||
if len(content_stripped) > 100 and all(c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r\t " for c in content_stripped[:200]):
|
||||
base64Data = content_stripped.replace("\n", "").replace("\r", "").replace("\t", "").replace(" ", "")
|
||||
else:
|
||||
base64Data = aiResponse.content
|
||||
else:
|
||||
base64Data = ""
|
||||
|
||||
# Always create proper JSON structure for images
|
||||
if base64Data:
|
||||
elements.append({
|
||||
"type": "image",
|
||||
"content": {
|
||||
|
|
@ -640,8 +884,17 @@ class StructureFiller:
|
|||
"caption": ""
|
||||
}
|
||||
})
|
||||
logger.debug(f"Created proper JSON image structure with base64Data length: {len(base64Data)}")
|
||||
else:
|
||||
logger.warning(f"IMAGE_GENERATE returned empty content for section {sectionId}")
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Image generation returned empty content",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
else:
|
||||
# Parse JSON response for other content types
|
||||
try:
|
||||
generatedElements = json.loads(
|
||||
self.services.utils.jsonExtractString(aiResponse.content)
|
||||
)
|
||||
|
|
@ -649,10 +902,27 @@ class StructureFiller:
|
|||
elements.extend(generatedElements)
|
||||
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
||||
elements.extend(generatedElements["elements"])
|
||||
elif isinstance(generatedElements, dict) and generatedElements.get("type"):
|
||||
elements.append(generatedElements)
|
||||
except (json.JSONDecodeError, ValueError) as json_error:
|
||||
logger.error(f"Error parsing JSON response for section {sectionId}: {str(json_error)}")
|
||||
elements.append({
|
||||
"type": "error",
|
||||
"message": f"Failed to parse JSON response: {str(json_error)}",
|
||||
"sectionId": sectionId
|
||||
})
|
||||
|
||||
# ChatLog abschließen
|
||||
self.services.chat.progressLogFinish(sectionOperationId, True)
|
||||
|
||||
# Update chapter progress after section completion
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
||||
self.services.chat.progressLogFinish(sectionOperationId, False)
|
||||
|
|
@ -662,6 +932,13 @@ class StructureFiller:
|
|||
"sectionId": sectionId
|
||||
})
|
||||
logger.error(f"Error generating section {sectionId}: {str(e)}")
|
||||
# Still update chapter progress even on error
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed (with errors)"
|
||||
)
|
||||
# NICHT raise - Section wird mit Fehlermeldung gerendert
|
||||
else:
|
||||
# Füge extrahierten Content direkt hinzu (kein AI-Call)
|
||||
|
|
@ -687,8 +964,36 @@ class StructureFiller:
|
|||
"extractionPrompt": part.metadata.get("extractionPrompt")
|
||||
})
|
||||
|
||||
# Assign elements to section (for all processing paths)
|
||||
section["elements"] = elements
|
||||
|
||||
# Update chapter progress after section completion (for all sections, including non-AI)
|
||||
chapterProgress = (sectionIndex + 1) / totalSections if totalSections > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
chapterOperationId,
|
||||
chapterProgress,
|
||||
f"Section {sectionIndex + 1}/{totalSections} completed"
|
||||
)
|
||||
|
||||
# Update overall progress after section completion
|
||||
overallProgress = calculateOverallProgress(chapterIndex - 1, totalChapters, sectionIndex + 1, totalSections)
|
||||
self.services.chat.progressLogUpdate(
|
||||
fillOperationId,
|
||||
overallProgress,
|
||||
f"Chapter {chapterIndex}/{totalChapters}, Section {sectionIndex + 1}/{totalSections} completed"
|
||||
)
|
||||
|
||||
# Finish chapter operation after all sections processed
|
||||
self.services.chat.progressLogFinish(chapterOperationId, True)
|
||||
|
||||
# Update overall progress after chapter completion
|
||||
overallProgress = chapterIndex / totalChapters if totalChapters > 0 else 1.0
|
||||
self.services.chat.progressLogUpdate(
|
||||
fillOperationId,
|
||||
overallProgress,
|
||||
f"Chapter {chapterIndex}/{totalChapters} completed: {chapterTitle}"
|
||||
)
|
||||
|
||||
return chapterStructure
|
||||
|
||||
def _addContentPartsMetadata(
|
||||
|
|
@ -744,7 +1049,10 @@ class StructureFiller:
|
|||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Flattening: Konvertiert Chapters zu finaler Section-Struktur.
|
||||
Jedes Chapter wird zu einer Heading-Section + dessen Sections.
|
||||
Jedes Chapter wird zu einer Heading-Section (Level 1) + dessen Sections.
|
||||
|
||||
IMPORTANT: Chapters are the main structure elements (heading level 1).
|
||||
All section headings with level < 2 are adjusted to level 2.
|
||||
"""
|
||||
result = {
|
||||
"metadata": chapterStructure.get("metadata", {}),
|
||||
|
|
@ -760,7 +1068,7 @@ class StructureFiller:
|
|||
}
|
||||
|
||||
for chapter in doc.get("chapters", []):
|
||||
# 1. Vordefinierte Heading-Section für Chapter-Title
|
||||
# 1. Vordefinierte Heading-Section für Chapter-Title (ALWAYS Level 1)
|
||||
heading_section = {
|
||||
"id": f"{chapter['id']}_heading",
|
||||
"content_type": "heading",
|
||||
|
|
@ -768,19 +1076,42 @@ class StructureFiller:
|
|||
"type": "heading",
|
||||
"content": {
|
||||
"text": chapter.get("title", ""),
|
||||
"level": chapter.get("level", 1)
|
||||
"level": 1 # Chapters are always level 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
flattened_doc["sections"].append(heading_section)
|
||||
|
||||
# 2. Generierte Sections
|
||||
flattened_doc["sections"].extend(chapter.get("sections", []))
|
||||
# 2. Generierte Sections - adjust heading levels
|
||||
for section in chapter.get("sections", []):
|
||||
adjusted_section = self._adjustSectionHeadingLevels(section)
|
||||
flattened_doc["sections"].append(adjusted_section)
|
||||
|
||||
result["documents"].append(flattened_doc)
|
||||
|
||||
return result
|
||||
|
||||
def _adjustSectionHeadingLevels(self, section: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Adjust heading levels in sections: sections with type heading and level < 2 are changed to level 2.
|
||||
Only chapter headings have level 1.
|
||||
"""
|
||||
adjusted_section = copy.deepcopy(section)
|
||||
|
||||
# Check if this is a heading section
|
||||
if adjusted_section.get("content_type") == "heading":
|
||||
elements = adjusted_section.get("elements", [])
|
||||
for element in elements:
|
||||
if isinstance(element, dict) and element.get("type") == "heading":
|
||||
content = element.get("content", {})
|
||||
if isinstance(content, dict):
|
||||
level = content.get("level", 1)
|
||||
# If level < 2, change to level 2 (only chapters have level 1)
|
||||
if level < 2:
|
||||
content["level"] = 2
|
||||
|
||||
return adjusted_section
|
||||
|
||||
def _buildChapterSectionsStructurePrompt(
|
||||
self,
|
||||
chapterId: str,
|
||||
|
|
@ -975,6 +1306,9 @@ CRITICAL: Return ONLY valid JSON. Do not include any explanatory text outside th
|
|||
|
||||
contentStructureExample = self._getContentStructureExample(contentType)
|
||||
|
||||
# Special handling for image content type with IMAGE_GENERATE
|
||||
isImageGeneration = contentType == "image" and len(validParts) == 0
|
||||
|
||||
if isAggregation:
|
||||
prompt = f"""# TASK: Generate Section Content (Aggregation)
|
||||
|
||||
|
|
@ -982,22 +1316,10 @@ CRITICAL: Return ONLY valid JSON. Do not include any explanatory text outside th
|
|||
- Section ID: {sectionId}
|
||||
- Content Type: {contentType}
|
||||
- Generation Hint: {generationHint}
|
||||
{contextText}
|
||||
|
||||
## USER REQUEST (for context)
|
||||
```
|
||||
{userPrompt}
|
||||
```
|
||||
|
||||
## AVAILABLE CONTENT FOR THIS SECTION
|
||||
{contentPartsText if contentPartsText else "(No content parts specified for this section)"}
|
||||
|
||||
## IMPORTANT - SECTION INDEPENDENCE:
|
||||
- This section is independent and self-contained
|
||||
- You do NOT have information about other sections' content
|
||||
- Provide all necessary context within this section
|
||||
- Context above is for logical flow only, NOT for content dependencies
|
||||
|
||||
## INSTRUCTIONS
|
||||
1. Generate content for section "{sectionId}" based on the generation hint above
|
||||
2. **AGGREGATION**: Combine ALL provided ContentParts into ONE element (e.g., one table with all data)
|
||||
|
|
@ -1007,6 +1329,10 @@ CRITICAL: Return ONLY valid JSON. Do not include any explanatory text outside th
|
|||
6. Ensure the generated content is self-contained and understandable independently
|
||||
7. Return ONLY a JSON object with an "elements" array
|
||||
8. Each element should match the content_type: {contentType}
|
||||
9. CRITICAL - NO HTML/STYLING: Do NOT include HTML tags, CSS styles, or any formatting markup in text content. Return plain text only. Formatting is handled automatically by the renderer.
|
||||
10. For paragraphs: Return plain text only, no HTML tags like <div>, <span>, <p>, or style attributes
|
||||
11. For headings: Return plain text only, no HTML tags or styling
|
||||
12. For images: Do NOT include base64 data in JSON - images are handled separately
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return a JSON object with this structure:
|
||||
|
|
@ -1020,7 +1346,16 @@ Return a JSON object with this structure:
|
|||
]
|
||||
}}
|
||||
|
||||
CRITICAL: "content" MUST always be an object (never a string). Return ONLY valid JSON. Do not include any explanatory text outside the JSON.
|
||||
CRITICAL:
|
||||
- "content" MUST always be an object (never a string)
|
||||
- For text content: Return plain text only, NO HTML tags, NO CSS styles, NO formatting markup
|
||||
- Return ONLY valid JSON. Do not include any explanatory text outside the JSON.
|
||||
|
||||
## CONTEXT (for reference only)
|
||||
{contextText if contextText else ""}
|
||||
```
|
||||
{userPrompt}
|
||||
```
|
||||
"""
|
||||
else:
|
||||
prompt = f"""# TASK: Generate Section Content
|
||||
|
|
@ -1029,30 +1364,21 @@ CRITICAL: "content" MUST always be an object (never a string). Return ONLY valid
|
|||
- Section ID: {sectionId}
|
||||
- Content Type: {contentType}
|
||||
- Generation Hint: {generationHint}
|
||||
{contextText}
|
||||
|
||||
## USER REQUEST (for context)
|
||||
```
|
||||
{userPrompt}
|
||||
```
|
||||
|
||||
## AVAILABLE CONTENT FOR THIS SECTION
|
||||
{contentPartsText if contentPartsText else "(No content parts specified for this section)"}
|
||||
|
||||
## IMPORTANT - SECTION INDEPENDENCE:
|
||||
- This section is independent and self-contained
|
||||
- You do NOT have information about other sections' content
|
||||
- Provide all necessary context within this section
|
||||
- Context above is for logical flow only, NOT for content dependencies
|
||||
|
||||
## INSTRUCTIONS
|
||||
1. Generate content for section "{sectionId}" based on the generation hint above
|
||||
2. Use the available content parts to populate this section
|
||||
3. For images: Use data URI format (data:image/[type];base64,[data]) when embedding base64 image data
|
||||
4. For extracted text: Format appropriately based on content_type ({contentType})
|
||||
5. Ensure the generated content is self-contained and understandable independently
|
||||
6. Return ONLY a JSON object with an "elements" array
|
||||
7. Each element should match the content_type: {contentType}
|
||||
3. For extracted text: Format appropriately based on content_type ({contentType})
|
||||
4. Ensure the generated content is self-contained and understandable independently
|
||||
5. Return ONLY a JSON object with an "elements" array
|
||||
6. Each element should match the content_type: {contentType}
|
||||
7. CRITICAL - NO HTML/STYLING: Do NOT include HTML tags, CSS styles, or any formatting markup in text content. Return plain text only. Formatting is handled automatically by the renderer.
|
||||
8. For paragraphs: Return plain text only, no HTML tags like <div>, <span>, <p>, or style attributes
|
||||
9. For headings: Return plain text only, no HTML tags or styling
|
||||
10. For images: If you need to reference an image, describe it in altText. Do NOT include base64 data - images are handled separately
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return a JSON object with this structure:
|
||||
|
|
@ -1066,7 +1392,16 @@ Return a JSON object with this structure:
|
|||
]
|
||||
}}
|
||||
|
||||
CRITICAL: "content" MUST always be an object (never a string). Return ONLY valid JSON. Do not include any explanatory text outside the JSON.
|
||||
CRITICAL:
|
||||
- "content" MUST always be an object (never a string)
|
||||
- For text content: Return plain text only, NO HTML tags, NO CSS styles, NO formatting markup
|
||||
- Return ONLY valid JSON. Do not include any explanatory text outside the JSON
|
||||
|
||||
## CONTEXT (for reference only)
|
||||
{contextText if contextText else ""}
|
||||
```
|
||||
{userPrompt}
|
||||
```
|
||||
"""
|
||||
return prompt
|
||||
|
||||
|
|
|
|||
|
|
@ -160,18 +160,30 @@ IMPORTANT - CHAPTER INDEPENDENCE:
|
|||
- One chapter does NOT have information about another chapter
|
||||
- Each chapter must provide its own context and be understandable alone
|
||||
|
||||
CRITICAL - CONTENT ASSIGNMENT TO CHAPTERS:
|
||||
- You MUST assign available ContentParts to chapters using contentPartIds
|
||||
- Based on the user request, determine which content should be used in which chapter
|
||||
- If the user request mentions specific content, assign the corresponding ContentPart to the appropriate chapter
|
||||
- Chapters WITHOUT contentPartIds can only generate generic content, NOT document-specific analysis
|
||||
- To include document content analysis, chapters MUST have contentPartIds assigned
|
||||
- Review the user request carefully to match ContentParts to chapters based on context and purpose
|
||||
|
||||
CRITICAL - CHAPTERS WITHOUT CONTENT PARTS:
|
||||
- If contentPartIds is EMPTY, generationHint MUST be VERY DETAILED with all context needed to generate content from scratch
|
||||
- Include: what to generate, what information to include, purpose, specific details
|
||||
- Without content parts, AI relies ENTIRELY on generationHint
|
||||
- GOOD: "Create [specific content] with [details]. Include [information]. Purpose: [explanation]."
|
||||
- BAD: "Create title" or "Add section" (too vague)
|
||||
- Without content parts, AI relies ENTIRELY on generationHint and CANNOT analyze document content
|
||||
|
||||
IMPORTANT - FORMATTING:
|
||||
- Formatting (fonts, colors, layouts, styles) is handled AUTOMATICALLY by the renderer
|
||||
- Do NOT specify formatting details in generationHint unless it's content-specific (e.g., "pie chart with 3 segments")
|
||||
- Focus on CONTENT and STRUCTURE, not visual formatting
|
||||
- The renderer will apply appropriate styling based on the output format ({outputFormat})
|
||||
|
||||
For each chapter:
|
||||
- chapter id
|
||||
- level (1, 2, 3, etc.)
|
||||
- title
|
||||
- contentPartIds: [List of ContentPart IDs]
|
||||
- contentPartIds: [List of ContentPart IDs] - ASSIGN content based on user request and chapter purpose
|
||||
- contentPartInstructions: {{
|
||||
"partId": {{
|
||||
"instruction": "How content should be structured"
|
||||
|
|
@ -179,6 +191,7 @@ For each chapter:
|
|||
}}
|
||||
- generationHint: Description of the content (must be self-contained with all necessary context)
|
||||
* If contentPartIds is EMPTY, generationHint MUST be VERY DETAILED with all context needed to generate content from scratch
|
||||
* Focus on content and structure, NOT formatting details
|
||||
|
||||
OUTPUT FORMAT: {outputFormat}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -535,6 +535,45 @@ class RendererXlsx(BaseRenderer):
|
|||
self.logger.warning(f"AI styling failed: {str(e)}, using defaults")
|
||||
return defaultStyles
|
||||
|
||||
def _getSafeAlignment(self, alignValue: Any) -> str:
|
||||
"""Get safe alignment value for openpyxl. Valid values: 'left', 'general', 'distributed', 'fill', 'justify', 'center', 'right', 'centerContinuous'."""
|
||||
if not alignValue:
|
||||
return "left"
|
||||
|
||||
alignStr = str(alignValue).lower().strip()
|
||||
|
||||
# Map common alignment values to openpyxl values
|
||||
alignmentMap = {
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"center": "center",
|
||||
"centre": "center",
|
||||
"general": "general",
|
||||
"distributed": "distributed",
|
||||
"fill": "fill",
|
||||
"justify": "justify",
|
||||
"centercontinuous": "centerContinuous",
|
||||
"center-continuous": "centerContinuous",
|
||||
"start": "left",
|
||||
"end": "right",
|
||||
"middle": "center"
|
||||
}
|
||||
|
||||
# Check direct mapping
|
||||
if alignStr in alignmentMap:
|
||||
return alignmentMap[alignStr]
|
||||
|
||||
# Check if it contains alignment keywords
|
||||
if "left" in alignStr or "start" in alignStr:
|
||||
return "left"
|
||||
elif "right" in alignStr or "end" in alignStr:
|
||||
return "right"
|
||||
elif "center" in alignStr or "centre" in alignStr or "middle" in alignStr:
|
||||
return "center"
|
||||
|
||||
# Default to left if unknown
|
||||
return "left"
|
||||
|
||||
def _getSafeColor(self, colorValue: str, default: str = "FF000000") -> str:
|
||||
"""Get a safe aRGB color value for Excel (without # prefix)."""
|
||||
if not isinstance(colorValue, str):
|
||||
|
|
@ -603,30 +642,34 @@ class RendererXlsx(BaseRenderer):
|
|||
return sanitized[:31]
|
||||
|
||||
def _generateSheetNamesFromContent(self, jsonContent: Dict[str, Any]) -> List[str]:
|
||||
"""Generate sheet names: each heading section creates a new tab."""
|
||||
"""Generate sheet names: each heading level 1 (chapter) creates a new tab."""
|
||||
sections = self._extractSections(jsonContent)
|
||||
|
||||
# If no sections, create a single sheet
|
||||
if not sections:
|
||||
return ["Content"]
|
||||
|
||||
# Simple logic: each heading section creates a new tab
|
||||
# Only heading level 1 (chapters) create new tabs
|
||||
sheetNames = []
|
||||
for section in sections:
|
||||
if section.get("content_type") == "heading":
|
||||
# Extract heading text from elements
|
||||
# Extract heading text and level from elements
|
||||
elements = section.get("elements", [])
|
||||
if elements and isinstance(elements, list) and len(elements) > 0:
|
||||
headingElement = elements[0]
|
||||
content = headingElement.get("content", {})
|
||||
if isinstance(content, dict):
|
||||
headingText = content.get("text", "")
|
||||
level = content.get("level", 1)
|
||||
elif isinstance(content, str):
|
||||
headingText = content
|
||||
level = 1
|
||||
else:
|
||||
headingText = ""
|
||||
level = 1
|
||||
|
||||
if headingText:
|
||||
# Only level 1 headings (chapters) create tabs
|
||||
if headingText and level == 1:
|
||||
sanitized_name = self._sanitizeSheetName(headingText)
|
||||
# Ensure unique sheet names
|
||||
if sanitized_name not in sheetNames:
|
||||
|
|
@ -639,7 +682,7 @@ class RendererXlsx(BaseRenderer):
|
|||
counter += 1
|
||||
sheetNames.append(f"{base_name} ({counter})"[:31])
|
||||
|
||||
# If no headings found, use document title
|
||||
# If no level 1 headings found, use document title
|
||||
if not sheetNames:
|
||||
documentTitle = jsonContent.get("metadata", {}).get("title", "Document")
|
||||
sheetNames.append(self._sanitizeSheetName(documentTitle))
|
||||
|
|
@ -647,7 +690,7 @@ class RendererXlsx(BaseRenderer):
|
|||
return sheetNames
|
||||
|
||||
def _populateExcelSheets(self, sheets: Dict[str, Any], jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> None:
|
||||
"""Populate Excel sheets: each heading creates a new tab, all following content goes in that tab."""
|
||||
"""Populate Excel sheets: each heading level 1 (chapter) creates a new tab, all following content goes in that tab."""
|
||||
try:
|
||||
# Get the actual sheet names that were created (keys are lowercase)
|
||||
sheetNames = list(sheets.keys())
|
||||
|
|
@ -657,7 +700,7 @@ class RendererXlsx(BaseRenderer):
|
|||
|
||||
sections = self._extractSections(jsonContent)
|
||||
|
||||
# Simple logic: iterate through sections, each heading creates a new tab
|
||||
# Only heading level 1 (chapters) create new tabs
|
||||
currentSheetIndex = 0
|
||||
currentSheet = None
|
||||
currentRow = 1
|
||||
|
|
@ -665,8 +708,19 @@ class RendererXlsx(BaseRenderer):
|
|||
for section in sections:
|
||||
contentType = section.get("content_type", "paragraph")
|
||||
|
||||
# Heading section: switch to next sheet
|
||||
# Heading section: check if it's level 1 (chapter) to switch to next sheet
|
||||
if contentType == "heading":
|
||||
# Extract level from heading element
|
||||
elements = section.get("elements", [])
|
||||
level = 1 # Default
|
||||
if elements and isinstance(elements, list) and len(elements) > 0:
|
||||
headingElement = elements[0]
|
||||
content = headingElement.get("content", {})
|
||||
if isinstance(content, dict):
|
||||
level = content.get("level", 1)
|
||||
|
||||
# Only level 1 headings (chapters) create new tabs
|
||||
if level == 1:
|
||||
if currentSheetIndex < len(sheetNames):
|
||||
sheetName = sheetNames[currentSheetIndex]
|
||||
currentSheet = sheets[sheetName] # sheets dict uses lowercase keys
|
||||
|
|
@ -695,7 +749,7 @@ class RendererXlsx(BaseRenderer):
|
|||
sheet['A1'] = sheetTitle
|
||||
title_style = styles.get("title", {})
|
||||
sheet['A1'].font = Font(size=16, bold=True, color=self._getSafeColor(title_style.get("color", "FF1F4E79")))
|
||||
sheet['A1'].alignment = Alignment(horizontal=title_style.get("align", "left"))
|
||||
sheet['A1'].alignment = Alignment(horizontal=self._getSafeAlignment(title_style.get("align", "left")))
|
||||
|
||||
# Get table data from elements (canonical JSON format)
|
||||
elements = section.get("elements", [])
|
||||
|
|
@ -707,8 +761,13 @@ class RendererXlsx(BaseRenderer):
|
|||
headers = []
|
||||
rows = []
|
||||
else:
|
||||
headers = content.get("headers", [])
|
||||
rows = content.get("rows", [])
|
||||
headers = content.get("headers") or []
|
||||
rows = content.get("rows") or []
|
||||
# Ensure headers and rows are lists
|
||||
if not isinstance(headers, list):
|
||||
headers = []
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
else:
|
||||
headers = []
|
||||
rows = []
|
||||
|
|
@ -770,11 +829,11 @@ class RendererXlsx(BaseRenderer):
|
|||
try:
|
||||
safe_color = self._getSafeColor(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"])
|
||||
sheet['A1'].alignment = Alignment(horizontal=self._getSafeAlignment(title_style["align"]))
|
||||
except Exception as 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"])
|
||||
sheet['A1'].alignment = Alignment(horizontal=self._getSafeAlignment(title_style["align"]))
|
||||
|
||||
# Generation info
|
||||
sheet['A3'] = "Generated:"
|
||||
|
|
@ -892,6 +951,8 @@ class RendererXlsx(BaseRenderer):
|
|||
startRow = self._addHeadingToExcel(sheet, element, styles, startRow)
|
||||
elif element_type == "image":
|
||||
startRow = self._addImageToExcel(sheet, element, styles, startRow)
|
||||
elif element_type == "code_block" or element_type == "code":
|
||||
startRow = self._addCodeBlockToExcel(sheet, element, styles, startRow)
|
||||
else:
|
||||
# Fallback: if element_type not set, use section_type
|
||||
if section_type == "table":
|
||||
|
|
@ -904,6 +965,8 @@ class RendererXlsx(BaseRenderer):
|
|||
startRow = self._addHeadingToExcel(sheet, element, styles, startRow)
|
||||
elif section_type == "image":
|
||||
startRow = self._addImageToExcel(sheet, element, styles, startRow)
|
||||
elif section_type == "code_block" or section_type == "code":
|
||||
startRow = self._addCodeBlockToExcel(sheet, element, styles, startRow)
|
||||
else:
|
||||
startRow = self._addParagraphToExcel(sheet, element, styles, startRow)
|
||||
|
||||
|
|
@ -943,9 +1006,16 @@ class RendererXlsx(BaseRenderer):
|
|||
content = element.get("content", {})
|
||||
if not isinstance(content, dict):
|
||||
return startRow
|
||||
|
||||
headers = content.get("headers", [])
|
||||
rows = content.get("rows", [])
|
||||
|
||||
# Ensure headers and rows are lists
|
||||
if not isinstance(headers, list):
|
||||
headers = []
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
|
||||
if not headers and not rows:
|
||||
return startRow
|
||||
|
||||
|
|
@ -965,12 +1035,21 @@ class RendererXlsx(BaseRenderer):
|
|||
sanitized_header = self._sanitizeCellValue(header)
|
||||
cell = sheet.cell(row=headerRow, column=col, value=sanitized_header)
|
||||
|
||||
# Apply styling with fallbacks - don't let styling errors prevent data rendering
|
||||
try:
|
||||
# Font styling
|
||||
cell.font = Font(
|
||||
bold=header_style.get("bold", True),
|
||||
color=self._getSafeColor(header_style.get("text_color", "FF000000"))
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to default font if styling fails
|
||||
try:
|
||||
cell.font = Font(bold=True, color=self._getSafeColor("FF000000"))
|
||||
except Exception:
|
||||
pass # Continue even if font fails
|
||||
|
||||
try:
|
||||
# Background color
|
||||
if header_style.get("background"):
|
||||
cell.fill = PatternFill(
|
||||
|
|
@ -978,15 +1057,27 @@ class RendererXlsx(BaseRenderer):
|
|||
end_color=self._getSafeColor(header_style["background"]),
|
||||
fill_type="solid"
|
||||
)
|
||||
except Exception:
|
||||
pass # Continue without background color if it fails
|
||||
|
||||
try:
|
||||
# Alignment
|
||||
cell.alignment = Alignment(
|
||||
horizontal=header_style.get("align", "left"),
|
||||
horizontal=self._getSafeAlignment(header_style.get("align", "left")),
|
||||
vertical="center"
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to default alignment if it fails
|
||||
try:
|
||||
cell.alignment = Alignment(horizontal="left", vertical="center")
|
||||
except Exception:
|
||||
pass # Continue even if alignment fails
|
||||
|
||||
try:
|
||||
# Border
|
||||
cell.border = thin_border
|
||||
except Exception:
|
||||
pass # Continue without border if it fails
|
||||
|
||||
startRow += 1
|
||||
|
||||
|
|
@ -1005,18 +1096,32 @@ class RendererXlsx(BaseRenderer):
|
|||
sanitized_value = self._sanitizeCellValue(cell_value)
|
||||
cell = sheet.cell(row=startRow, column=col, value=sanitized_value)
|
||||
|
||||
# Apply styling with fallbacks - don't let styling errors prevent data rendering
|
||||
try:
|
||||
# Font styling
|
||||
if cell_style.get("text_color"):
|
||||
cell.font = Font(color=self._getSafeColor(cell_style["text_color"]))
|
||||
except Exception:
|
||||
pass # Continue without font color if it fails
|
||||
|
||||
try:
|
||||
# Alignment
|
||||
cell.alignment = Alignment(
|
||||
horizontal=cell_style.get("align", "left"),
|
||||
horizontal=self._getSafeAlignment(cell_style.get("align", "left")),
|
||||
vertical="center"
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to default alignment if it fails
|
||||
try:
|
||||
cell.alignment = Alignment(horizontal="left", vertical="center")
|
||||
except Exception:
|
||||
pass # Continue even if alignment fails
|
||||
|
||||
try:
|
||||
# Border
|
||||
cell.border = thin_border
|
||||
except Exception:
|
||||
pass # Continue without border if it fails
|
||||
|
||||
startRow += 1
|
||||
|
||||
|
|
@ -1038,7 +1143,10 @@ class RendererXlsx(BaseRenderer):
|
|||
content = element.get("content", {})
|
||||
if not isinstance(content, dict):
|
||||
return startRow
|
||||
list_items = content.get("items", [])
|
||||
list_items = content.get("items") or []
|
||||
# Ensure list_items is a list
|
||||
if not isinstance(list_items, list):
|
||||
list_items = []
|
||||
|
||||
list_style = styles.get("bullet_list", {})
|
||||
for item in list_items:
|
||||
|
|
@ -1200,6 +1308,52 @@ class RendererXlsx(BaseRenderer):
|
|||
errorCell.font = Font(color="FFFF0000", italic=True) # Red color
|
||||
return startRow + 1
|
||||
|
||||
def _addCodeBlockToExcel(self, sheet, element: Dict[str, Any], styles: Dict[str, Any], startRow: int) -> int:
|
||||
"""Add a code block element to Excel sheet. Expects nested content structure."""
|
||||
try:
|
||||
# Extract from nested content structure
|
||||
content = element.get("content", {})
|
||||
if not isinstance(content, dict):
|
||||
return startRow
|
||||
code = content.get("code", "")
|
||||
language = content.get("language", "")
|
||||
|
||||
if code:
|
||||
code_style = styles.get("code_block", {})
|
||||
|
||||
# Add language label if present
|
||||
if language:
|
||||
langCell = sheet.cell(row=startRow, column=1, value=f"Code ({language}):")
|
||||
langCell.font = Font(bold=True, color=self._getSafeColor(code_style.get("color", "FF000000")))
|
||||
startRow += 1
|
||||
|
||||
# Split code into lines and add each line
|
||||
code_lines = code.split('\n')
|
||||
for line in code_lines:
|
||||
codeCell = sheet.cell(row=startRow, column=1, value=line)
|
||||
codeCell.font = Font(
|
||||
name=code_style.get("font", "Courier New"),
|
||||
size=code_style.get("font_size", 10),
|
||||
color=self._getSafeColor(code_style.get("color", "FF2F2F2F"))
|
||||
)
|
||||
# Set background color if specified
|
||||
if code_style.get("background"):
|
||||
codeCell.fill = PatternFill(
|
||||
start_color=self._getSafeColor(code_style["background"]),
|
||||
end_color=self._getSafeColor(code_style["background"]),
|
||||
fill_type="solid"
|
||||
)
|
||||
startRow += 1
|
||||
|
||||
# Add spacing after code block
|
||||
startRow += 1
|
||||
|
||||
return startRow
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not add code block to Excel: {str(e)}")
|
||||
return startRow + 1
|
||||
|
||||
def _formatTimestamp(self) -> str:
|
||||
"""Format current timestamp for document generation."""
|
||||
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||
|
|
|
|||
|
|
@ -413,10 +413,12 @@ class DocumentGenerationFormatsTester10:
|
|||
async def testAllFormats(self) -> Dict[str, Any]:
|
||||
"""Test document generation in DOCX, XLSX, PPTX, PDF, and HTML formats."""
|
||||
print("\n" + "="*80)
|
||||
print("TESTING DOCUMENT GENERATION IN DOCX, XLSX, PPTX, PDF, AND HTML FORMATS")
|
||||
print("TESTING DOCUMENT GENERATION IN HTML FORMAT")
|
||||
print("="*80)
|
||||
|
||||
formats = ["docx", "xlsx", "pptx", "pdf", "html"]
|
||||
# Only test HTML format
|
||||
formats = ["html"]
|
||||
# formats = ["docx", "xlsx", "pptx", "pdf", "html"] # Commented out other formats
|
||||
results = {}
|
||||
|
||||
for format in formats:
|
||||
|
|
@ -469,7 +471,7 @@ class DocumentGenerationFormatsTester10:
|
|||
async def runTest(self):
|
||||
"""Run the complete test."""
|
||||
print("\n" + "="*80)
|
||||
print("DOCUMENT GENERATION FORMATS TEST 10 - DOCX, XLSX, PPTX, PDF, HTML")
|
||||
print("DOCUMENT GENERATION FORMATS TEST 10 - HTML ONLY")
|
||||
print("="*80)
|
||||
|
||||
try:
|
||||
|
|
|
|||
Loading…
Reference in a new issue