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).
|
Phase 5D.1: Generiert Sections-Struktur für jedes Chapter (ohne Content).
|
||||||
Sections enthalten: content_type, contentPartIds, generationHint, useAiCall
|
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 doc in chapterStructure.get("documents", []):
|
||||||
for chapter in doc.get("chapters", []):
|
for chapter in doc.get("chapters", []):
|
||||||
|
chapterIndex += 1
|
||||||
chapterId = chapter.get("id", "unknown")
|
chapterId = chapter.get("id", "unknown")
|
||||||
chapterLevel = chapter.get("level", 1)
|
chapterLevel = chapter.get("level", 1)
|
||||||
chapterTitle = chapter.get("title", "")
|
chapterTitle = chapter.get("title", "Untitled Chapter")
|
||||||
generationHint = chapter.get("generationHint", "")
|
generationHint = chapter.get("generationHint", "")
|
||||||
contentPartIds = chapter.get("contentPartIds", [])
|
contentPartIds = chapter.get("contentPartIds", [])
|
||||||
contentPartInstructions = chapter.get("contentPartInstructions", {})
|
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(
|
chapterPrompt = self._buildChapterSectionsStructurePrompt(
|
||||||
chapterId=chapterId,
|
chapterId=chapterId,
|
||||||
chapterLevel=chapterLevel,
|
chapterLevel=chapterLevel,
|
||||||
|
|
@ -194,19 +207,55 @@ class StructureFiller:
|
||||||
"""
|
"""
|
||||||
Phase 5D.2: Füllt Sections mit ContentParts.
|
Phase 5D.2: Füllt Sections mit ContentParts.
|
||||||
"""
|
"""
|
||||||
# Sammle alle Sections für sequenzielle Verarbeitung
|
# Sammle alle Sections für Kontext-Informationen (für alle Sections)
|
||||||
sections_to_process = []
|
all_sections_list = []
|
||||||
all_sections_list = [] # Für Kontext-Informationen
|
|
||||||
for doc in chapterStructure.get("documents", []):
|
for doc in chapterStructure.get("documents", []):
|
||||||
for chapter in doc.get("chapters", []):
|
for chapter in doc.get("chapters", []):
|
||||||
for section in chapter.get("sections", []):
|
for section in chapter.get("sections", []):
|
||||||
all_sections_list.append(section)
|
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
|
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")
|
sectionId = section.get("id")
|
||||||
|
sectionTitle = section.get("title", sectionId)
|
||||||
contentPartIds = section.get("contentPartIds", [])
|
contentPartIds = section.get("contentPartIds", [])
|
||||||
contentFormats = section.get("contentFormats", {})
|
contentFormats = section.get("contentFormats", {})
|
||||||
# Check both camelCase and snake_case for generationHint
|
# Check both camelCase and snake_case for generationHint
|
||||||
|
|
@ -214,6 +263,14 @@ class StructureFiller:
|
||||||
contentType = section.get("content_type", "paragraph")
|
contentType = section.get("content_type", "paragraph")
|
||||||
useAiCall = section.get("useAiCall", False)
|
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
|
# 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
|
# 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)
|
# (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
|
# Erstelle Operation-ID für Section-Generierung
|
||||||
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
||||||
|
|
||||||
# Starte ChatLog mit Parent-Referenz
|
# Starte ChatLog mit Parent-Referenz (chapter, not fillOperationId)
|
||||||
self.services.chat.progressLogStart(
|
self.services.chat.progressLogStart(
|
||||||
sectionOperationId,
|
sectionOperationId,
|
||||||
"Section Generation (Aggregation)",
|
"Section Generation (Aggregation)",
|
||||||
"Section",
|
f"Section {sectionIndex + 1}/{totalSections}",
|
||||||
f"Generating section {sectionId} with {len(extractedParts)} parts",
|
f"{sectionTitle} ({len(extractedParts)} parts)",
|
||||||
parentOperationId=fillOperationId
|
parentOperationId=chapterOperationId
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Update: Building prompt
|
||||||
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.2, "Building generation prompt")
|
||||||
|
|
||||||
# Debug: Log Prompt
|
# Debug: Log Prompt
|
||||||
self.services.utils.writeDebugFile(
|
self.services.utils.writeDebugFile(
|
||||||
generationPrompt,
|
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!)
|
# Verwende callAi für ContentParts-Unterstützung (nicht callAiPlanning!)
|
||||||
# Use IMAGE_GENERATE for image content type
|
# Use IMAGE_GENERATE for image content type
|
||||||
|
|
@ -344,29 +407,64 @@ class StructureFiller:
|
||||||
)
|
)
|
||||||
aiResponse = await self.aiService.callAi(request)
|
aiResponse = await self.aiService.callAi(request)
|
||||||
|
|
||||||
|
# Update: Processing response
|
||||||
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||||
|
|
||||||
# Debug: Log Response
|
# Debug: Log Response
|
||||||
self.services.utils.writeDebugFile(
|
self.services.utils.writeDebugFile(
|
||||||
aiResponse.content,
|
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
|
# Handle IMAGE_GENERATE differently - returns image data directly
|
||||||
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
||||||
import base64
|
import base64
|
||||||
|
base64Data = ""
|
||||||
|
|
||||||
# Convert image data to base64 string if needed
|
# Convert image data to base64 string if needed
|
||||||
if isinstance(aiResponse.content, bytes):
|
if isinstance(aiResponse.content, bytes):
|
||||||
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
||||||
elif isinstance(aiResponse.content, str):
|
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
|
# Already base64 string or data URI
|
||||||
if aiResponse.content.startswith("data:image/"):
|
if aiResponse.content.startswith("data:image/"):
|
||||||
# Extract base64 from data URI
|
# Extract base64 from data URI
|
||||||
base64Data = aiResponse.content.split(",", 1)[1]
|
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:
|
else:
|
||||||
base64Data = aiResponse.content
|
base64Data = aiResponse.content
|
||||||
else:
|
else:
|
||||||
base64Data = ""
|
base64Data = ""
|
||||||
|
|
||||||
|
# Always create proper JSON structure for images
|
||||||
|
if base64Data:
|
||||||
elements.append({
|
elements.append({
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"content": {
|
"content": {
|
||||||
|
|
@ -375,8 +473,17 @@ class StructureFiller:
|
||||||
"caption": ""
|
"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:
|
else:
|
||||||
# Parse JSON response for other content types
|
# Parse JSON response for other content types
|
||||||
|
try:
|
||||||
generatedElements = json.loads(
|
generatedElements = json.loads(
|
||||||
self.services.utils.jsonExtractString(aiResponse.content)
|
self.services.utils.jsonExtractString(aiResponse.content)
|
||||||
)
|
)
|
||||||
|
|
@ -384,10 +491,39 @@ class StructureFiller:
|
||||||
elements.extend(generatedElements)
|
elements.extend(generatedElements)
|
||||||
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
||||||
elements.extend(generatedElements["elements"])
|
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
|
# ChatLog abschließen
|
||||||
self.services.chat.progressLogFinish(sectionOperationId, True)
|
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:
|
except Exception as e:
|
||||||
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
||||||
self.services.chat.progressLogFinish(sectionOperationId, False)
|
self.services.chat.progressLogFinish(sectionOperationId, False)
|
||||||
|
|
@ -397,6 +533,13 @@ class StructureFiller:
|
||||||
"sectionId": sectionId
|
"sectionId": sectionId
|
||||||
})
|
})
|
||||||
logger.error(f"Error generating section {sectionId}: {str(e)}")
|
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
|
# NICHT raise - Section wird mit Fehlermeldung gerendert
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
@ -418,22 +561,28 @@ class StructureFiller:
|
||||||
# Erstelle Operation-ID für Section-Generierung
|
# Erstelle Operation-ID für Section-Generierung
|
||||||
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
||||||
|
|
||||||
# Starte ChatLog mit Parent-Referenz
|
# Starte ChatLog mit Parent-Referenz (chapter, not fillOperationId)
|
||||||
self.services.chat.progressLogStart(
|
self.services.chat.progressLogStart(
|
||||||
sectionOperationId,
|
sectionOperationId,
|
||||||
"Section Generation",
|
"Section Generation",
|
||||||
"Section",
|
f"Section {sectionIndex + 1}/{totalSections}",
|
||||||
f"Generating section {sectionId} from generationHint",
|
f"{sectionTitle} (from generationHint)",
|
||||||
parentOperationId=fillOperationId
|
parentOperationId=chapterOperationId
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Update: Building prompt
|
||||||
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.2, "Building generation prompt")
|
||||||
|
|
||||||
# Debug: Log Prompt
|
# Debug: Log Prompt
|
||||||
self.services.utils.writeDebugFile(
|
self.services.utils.writeDebugFile(
|
||||||
generationPrompt,
|
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
|
# Verwende callAi ohne ContentParts
|
||||||
operationType = OperationTypeEnum.IMAGE_GENERATE if contentType == "image" else OperationTypeEnum.DATA_ANALYSE
|
operationType = OperationTypeEnum.IMAGE_GENERATE if contentType == "image" else OperationTypeEnum.DATA_ANALYSE
|
||||||
|
|
@ -457,29 +606,57 @@ class StructureFiller:
|
||||||
)
|
)
|
||||||
aiResponse = await self.aiService.callAi(request)
|
aiResponse = await self.aiService.callAi(request)
|
||||||
|
|
||||||
|
# Update: Processing response
|
||||||
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||||
|
|
||||||
# Debug: Log Response
|
# Debug: Log Response
|
||||||
self.services.utils.writeDebugFile(
|
self.services.utils.writeDebugFile(
|
||||||
aiResponse.content,
|
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
|
# Handle IMAGE_GENERATE differently - returns image data directly
|
||||||
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
||||||
import base64
|
import base64
|
||||||
|
base64Data = ""
|
||||||
|
|
||||||
# Convert image data to base64 string if needed
|
# Convert image data to base64 string if needed
|
||||||
if isinstance(aiResponse.content, bytes):
|
if isinstance(aiResponse.content, bytes):
|
||||||
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
||||||
elif isinstance(aiResponse.content, str):
|
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
|
# Already base64 string or data URI
|
||||||
if aiResponse.content.startswith("data:image/"):
|
if aiResponse.content.startswith("data:image/"):
|
||||||
# Extract base64 from data URI
|
|
||||||
base64Data = aiResponse.content.split(",", 1)[1]
|
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:
|
else:
|
||||||
base64Data = aiResponse.content
|
base64Data = aiResponse.content
|
||||||
else:
|
else:
|
||||||
base64Data = ""
|
base64Data = ""
|
||||||
|
|
||||||
|
# Always create proper JSON structure for images
|
||||||
|
if base64Data:
|
||||||
elements.append({
|
elements.append({
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"content": {
|
"content": {
|
||||||
|
|
@ -488,8 +665,17 @@ class StructureFiller:
|
||||||
"caption": ""
|
"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:
|
else:
|
||||||
# Parse JSON response for other content types
|
# Parse JSON response for other content types
|
||||||
|
try:
|
||||||
generatedElements = json.loads(
|
generatedElements = json.loads(
|
||||||
self.services.utils.jsonExtractString(aiResponse.content)
|
self.services.utils.jsonExtractString(aiResponse.content)
|
||||||
)
|
)
|
||||||
|
|
@ -497,10 +683,27 @@ class StructureFiller:
|
||||||
elements.extend(generatedElements)
|
elements.extend(generatedElements)
|
||||||
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
||||||
elements.extend(generatedElements["elements"])
|
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
|
# ChatLog abschließen
|
||||||
self.services.chat.progressLogFinish(sectionOperationId, True)
|
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:
|
except Exception as e:
|
||||||
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
||||||
self.services.chat.progressLogFinish(sectionOperationId, False)
|
self.services.chat.progressLogFinish(sectionOperationId, False)
|
||||||
|
|
@ -510,6 +713,13 @@ class StructureFiller:
|
||||||
"sectionId": sectionId
|
"sectionId": sectionId
|
||||||
})
|
})
|
||||||
logger.error(f"Error generating section {sectionId}: {str(e)}")
|
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
|
# Einzelverarbeitung: Jeder Part einzeln
|
||||||
for partId in contentPartIds:
|
for partId in contentPartIds:
|
||||||
|
|
@ -567,22 +777,28 @@ class StructureFiller:
|
||||||
# Erstelle Operation-ID für Section-Generierung
|
# Erstelle Operation-ID für Section-Generierung
|
||||||
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
sectionOperationId = f"{fillOperationId}_section_{sectionId}"
|
||||||
|
|
||||||
# Starte ChatLog mit Parent-Referenz
|
# Starte ChatLog mit Parent-Referenz (chapter, not fillOperationId)
|
||||||
self.services.chat.progressLogStart(
|
self.services.chat.progressLogStart(
|
||||||
sectionOperationId,
|
sectionOperationId,
|
||||||
"Section Generation",
|
"Section Generation",
|
||||||
"Section",
|
f"Section {sectionIndex + 1}/{totalSections}",
|
||||||
f"Generating section {sectionId}",
|
f"{sectionTitle} (single part)",
|
||||||
parentOperationId=fillOperationId
|
parentOperationId=chapterOperationId
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Update: Building prompt
|
||||||
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.2, "Building generation prompt")
|
||||||
|
|
||||||
# Debug: Log Prompt
|
# Debug: Log Prompt
|
||||||
self.services.utils.writeDebugFile(
|
self.services.utils.writeDebugFile(
|
||||||
generationPrompt,
|
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
|
# Verwende callAi für ContentParts-Unterstützung
|
||||||
# Use IMAGE_GENERATE for image content type
|
# Use IMAGE_GENERATE for image content type
|
||||||
|
|
@ -609,29 +825,57 @@ class StructureFiller:
|
||||||
)
|
)
|
||||||
aiResponse = await self.aiService.callAi(request)
|
aiResponse = await self.aiService.callAi(request)
|
||||||
|
|
||||||
|
# Update: Processing response
|
||||||
|
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
|
||||||
|
|
||||||
# Debug: Log Response
|
# Debug: Log Response
|
||||||
self.services.utils.writeDebugFile(
|
self.services.utils.writeDebugFile(
|
||||||
aiResponse.content,
|
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
|
# Handle IMAGE_GENERATE differently - returns image data directly
|
||||||
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
if contentType == "image" and operationType == OperationTypeEnum.IMAGE_GENERATE:
|
||||||
import base64
|
import base64
|
||||||
|
base64Data = ""
|
||||||
|
|
||||||
# Convert image data to base64 string if needed
|
# Convert image data to base64 string if needed
|
||||||
if isinstance(aiResponse.content, bytes):
|
if isinstance(aiResponse.content, bytes):
|
||||||
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
base64Data = base64.b64encode(aiResponse.content).decode('utf-8')
|
||||||
elif isinstance(aiResponse.content, str):
|
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
|
# Already base64 string or data URI
|
||||||
if aiResponse.content.startswith("data:image/"):
|
if aiResponse.content.startswith("data:image/"):
|
||||||
# Extract base64 from data URI
|
|
||||||
base64Data = aiResponse.content.split(",", 1)[1]
|
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:
|
else:
|
||||||
base64Data = aiResponse.content
|
base64Data = aiResponse.content
|
||||||
else:
|
else:
|
||||||
base64Data = ""
|
base64Data = ""
|
||||||
|
|
||||||
|
# Always create proper JSON structure for images
|
||||||
|
if base64Data:
|
||||||
elements.append({
|
elements.append({
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"content": {
|
"content": {
|
||||||
|
|
@ -640,8 +884,17 @@ class StructureFiller:
|
||||||
"caption": ""
|
"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:
|
else:
|
||||||
# Parse JSON response for other content types
|
# Parse JSON response for other content types
|
||||||
|
try:
|
||||||
generatedElements = json.loads(
|
generatedElements = json.loads(
|
||||||
self.services.utils.jsonExtractString(aiResponse.content)
|
self.services.utils.jsonExtractString(aiResponse.content)
|
||||||
)
|
)
|
||||||
|
|
@ -649,10 +902,27 @@ class StructureFiller:
|
||||||
elements.extend(generatedElements)
|
elements.extend(generatedElements)
|
||||||
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
elif isinstance(generatedElements, dict) and "elements" in generatedElements:
|
||||||
elements.extend(generatedElements["elements"])
|
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
|
# ChatLog abschließen
|
||||||
self.services.chat.progressLogFinish(sectionOperationId, True)
|
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:
|
except Exception as e:
|
||||||
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
# Fehlerhafte Section mit Fehlermeldung rendern (kein Abbruch!)
|
||||||
self.services.chat.progressLogFinish(sectionOperationId, False)
|
self.services.chat.progressLogFinish(sectionOperationId, False)
|
||||||
|
|
@ -662,6 +932,13 @@ class StructureFiller:
|
||||||
"sectionId": sectionId
|
"sectionId": sectionId
|
||||||
})
|
})
|
||||||
logger.error(f"Error generating section {sectionId}: {str(e)}")
|
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
|
# NICHT raise - Section wird mit Fehlermeldung gerendert
|
||||||
else:
|
else:
|
||||||
# Füge extrahierten Content direkt hinzu (kein AI-Call)
|
# Füge extrahierten Content direkt hinzu (kein AI-Call)
|
||||||
|
|
@ -687,8 +964,36 @@ class StructureFiller:
|
||||||
"extractionPrompt": part.metadata.get("extractionPrompt")
|
"extractionPrompt": part.metadata.get("extractionPrompt")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Assign elements to section (for all processing paths)
|
||||||
section["elements"] = elements
|
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
|
return chapterStructure
|
||||||
|
|
||||||
def _addContentPartsMetadata(
|
def _addContentPartsMetadata(
|
||||||
|
|
@ -744,7 +1049,10 @@ class StructureFiller:
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Flattening: Konvertiert Chapters zu finaler Section-Struktur.
|
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 = {
|
result = {
|
||||||
"metadata": chapterStructure.get("metadata", {}),
|
"metadata": chapterStructure.get("metadata", {}),
|
||||||
|
|
@ -760,7 +1068,7 @@ class StructureFiller:
|
||||||
}
|
}
|
||||||
|
|
||||||
for chapter in doc.get("chapters", []):
|
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 = {
|
heading_section = {
|
||||||
"id": f"{chapter['id']}_heading",
|
"id": f"{chapter['id']}_heading",
|
||||||
"content_type": "heading",
|
"content_type": "heading",
|
||||||
|
|
@ -768,19 +1076,42 @@ class StructureFiller:
|
||||||
"type": "heading",
|
"type": "heading",
|
||||||
"content": {
|
"content": {
|
||||||
"text": chapter.get("title", ""),
|
"text": chapter.get("title", ""),
|
||||||
"level": chapter.get("level", 1)
|
"level": 1 # Chapters are always level 1
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
flattened_doc["sections"].append(heading_section)
|
flattened_doc["sections"].append(heading_section)
|
||||||
|
|
||||||
# 2. Generierte Sections
|
# 2. Generierte Sections - adjust heading levels
|
||||||
flattened_doc["sections"].extend(chapter.get("sections", []))
|
for section in chapter.get("sections", []):
|
||||||
|
adjusted_section = self._adjustSectionHeadingLevels(section)
|
||||||
|
flattened_doc["sections"].append(adjusted_section)
|
||||||
|
|
||||||
result["documents"].append(flattened_doc)
|
result["documents"].append(flattened_doc)
|
||||||
|
|
||||||
return result
|
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(
|
def _buildChapterSectionsStructurePrompt(
|
||||||
self,
|
self,
|
||||||
chapterId: str,
|
chapterId: str,
|
||||||
|
|
@ -975,6 +1306,9 @@ CRITICAL: Return ONLY valid JSON. Do not include any explanatory text outside th
|
||||||
|
|
||||||
contentStructureExample = self._getContentStructureExample(contentType)
|
contentStructureExample = self._getContentStructureExample(contentType)
|
||||||
|
|
||||||
|
# Special handling for image content type with IMAGE_GENERATE
|
||||||
|
isImageGeneration = contentType == "image" and len(validParts) == 0
|
||||||
|
|
||||||
if isAggregation:
|
if isAggregation:
|
||||||
prompt = f"""# TASK: Generate Section Content (Aggregation)
|
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}
|
- Section ID: {sectionId}
|
||||||
- Content Type: {contentType}
|
- Content Type: {contentType}
|
||||||
- Generation Hint: {generationHint}
|
- Generation Hint: {generationHint}
|
||||||
{contextText}
|
|
||||||
|
|
||||||
## USER REQUEST (for context)
|
|
||||||
```
|
|
||||||
{userPrompt}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AVAILABLE CONTENT FOR THIS SECTION
|
## AVAILABLE CONTENT FOR THIS SECTION
|
||||||
{contentPartsText if contentPartsText else "(No content parts specified 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
|
## INSTRUCTIONS
|
||||||
1. Generate content for section "{sectionId}" based on the generation hint above
|
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)
|
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
|
6. Ensure the generated content is self-contained and understandable independently
|
||||||
7. Return ONLY a JSON object with an "elements" array
|
7. Return ONLY a JSON object with an "elements" array
|
||||||
8. Each element should match the content_type: {contentType}
|
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
|
## OUTPUT FORMAT
|
||||||
Return a JSON object with this structure:
|
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:
|
else:
|
||||||
prompt = f"""# TASK: Generate Section Content
|
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}
|
- Section ID: {sectionId}
|
||||||
- Content Type: {contentType}
|
- Content Type: {contentType}
|
||||||
- Generation Hint: {generationHint}
|
- Generation Hint: {generationHint}
|
||||||
{contextText}
|
|
||||||
|
|
||||||
## USER REQUEST (for context)
|
|
||||||
```
|
|
||||||
{userPrompt}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AVAILABLE CONTENT FOR THIS SECTION
|
## AVAILABLE CONTENT FOR THIS SECTION
|
||||||
{contentPartsText if contentPartsText else "(No content parts specified 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
|
## INSTRUCTIONS
|
||||||
1. Generate content for section "{sectionId}" based on the generation hint above
|
1. Generate content for section "{sectionId}" based on the generation hint above
|
||||||
2. Use the available content parts to populate this section
|
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
|
3. For extracted text: Format appropriately based on content_type ({contentType})
|
||||||
4. For extracted text: Format appropriately based on content_type ({contentType})
|
4. Ensure the generated content is self-contained and understandable independently
|
||||||
5. Ensure the generated content is self-contained and understandable independently
|
5. Return ONLY a JSON object with an "elements" array
|
||||||
6. Return ONLY a JSON object with an "elements" array
|
6. Each element should match the content_type: {contentType}
|
||||||
7. 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
|
## OUTPUT FORMAT
|
||||||
Return a JSON object with this structure:
|
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
|
return prompt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,18 +160,30 @@ IMPORTANT - CHAPTER INDEPENDENCE:
|
||||||
- One chapter does NOT have information about another chapter
|
- One chapter does NOT have information about another chapter
|
||||||
- Each chapter must provide its own context and be understandable alone
|
- 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:
|
CRITICAL - CHAPTERS WITHOUT CONTENT PARTS:
|
||||||
- If contentPartIds is EMPTY, generationHint MUST be VERY DETAILED with all context needed to generate content from scratch
|
- 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
|
- Include: what to generate, what information to include, purpose, specific details
|
||||||
- Without content parts, AI relies ENTIRELY on generationHint
|
- Without content parts, AI relies ENTIRELY on generationHint and CANNOT analyze document content
|
||||||
- GOOD: "Create [specific content] with [details]. Include [information]. Purpose: [explanation]."
|
|
||||||
- BAD: "Create title" or "Add section" (too vague)
|
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:
|
For each chapter:
|
||||||
- chapter id
|
- chapter id
|
||||||
- level (1, 2, 3, etc.)
|
- level (1, 2, 3, etc.)
|
||||||
- title
|
- title
|
||||||
- contentPartIds: [List of ContentPart IDs]
|
- contentPartIds: [List of ContentPart IDs] - ASSIGN content based on user request and chapter purpose
|
||||||
- contentPartInstructions: {{
|
- contentPartInstructions: {{
|
||||||
"partId": {{
|
"partId": {{
|
||||||
"instruction": "How content should be structured"
|
"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)
|
- 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
|
* 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}
|
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")
|
self.logger.warning(f"AI styling failed: {str(e)}, using defaults")
|
||||||
return defaultStyles
|
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:
|
def _getSafeColor(self, colorValue: str, default: str = "FF000000") -> str:
|
||||||
"""Get a safe aRGB color value for Excel (without # prefix)."""
|
"""Get a safe aRGB color value for Excel (without # prefix)."""
|
||||||
if not isinstance(colorValue, str):
|
if not isinstance(colorValue, str):
|
||||||
|
|
@ -603,30 +642,34 @@ class RendererXlsx(BaseRenderer):
|
||||||
return sanitized[:31]
|
return sanitized[:31]
|
||||||
|
|
||||||
def _generateSheetNamesFromContent(self, jsonContent: Dict[str, Any]) -> List[str]:
|
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)
|
sections = self._extractSections(jsonContent)
|
||||||
|
|
||||||
# If no sections, create a single sheet
|
# If no sections, create a single sheet
|
||||||
if not sections:
|
if not sections:
|
||||||
return ["Content"]
|
return ["Content"]
|
||||||
|
|
||||||
# Simple logic: each heading section creates a new tab
|
# Only heading level 1 (chapters) create new tabs
|
||||||
sheetNames = []
|
sheetNames = []
|
||||||
for section in sections:
|
for section in sections:
|
||||||
if section.get("content_type") == "heading":
|
if section.get("content_type") == "heading":
|
||||||
# Extract heading text from elements
|
# Extract heading text and level from elements
|
||||||
elements = section.get("elements", [])
|
elements = section.get("elements", [])
|
||||||
if elements and isinstance(elements, list) and len(elements) > 0:
|
if elements and isinstance(elements, list) and len(elements) > 0:
|
||||||
headingElement = elements[0]
|
headingElement = elements[0]
|
||||||
content = headingElement.get("content", {})
|
content = headingElement.get("content", {})
|
||||||
if isinstance(content, dict):
|
if isinstance(content, dict):
|
||||||
headingText = content.get("text", "")
|
headingText = content.get("text", "")
|
||||||
|
level = content.get("level", 1)
|
||||||
elif isinstance(content, str):
|
elif isinstance(content, str):
|
||||||
headingText = content
|
headingText = content
|
||||||
|
level = 1
|
||||||
else:
|
else:
|
||||||
headingText = ""
|
headingText = ""
|
||||||
|
level = 1
|
||||||
|
|
||||||
if headingText:
|
# Only level 1 headings (chapters) create tabs
|
||||||
|
if headingText and level == 1:
|
||||||
sanitized_name = self._sanitizeSheetName(headingText)
|
sanitized_name = self._sanitizeSheetName(headingText)
|
||||||
# Ensure unique sheet names
|
# Ensure unique sheet names
|
||||||
if sanitized_name not in sheetNames:
|
if sanitized_name not in sheetNames:
|
||||||
|
|
@ -639,7 +682,7 @@ class RendererXlsx(BaseRenderer):
|
||||||
counter += 1
|
counter += 1
|
||||||
sheetNames.append(f"{base_name} ({counter})"[:31])
|
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:
|
if not sheetNames:
|
||||||
documentTitle = jsonContent.get("metadata", {}).get("title", "Document")
|
documentTitle = jsonContent.get("metadata", {}).get("title", "Document")
|
||||||
sheetNames.append(self._sanitizeSheetName(documentTitle))
|
sheetNames.append(self._sanitizeSheetName(documentTitle))
|
||||||
|
|
@ -647,7 +690,7 @@ class RendererXlsx(BaseRenderer):
|
||||||
return sheetNames
|
return sheetNames
|
||||||
|
|
||||||
def _populateExcelSheets(self, sheets: Dict[str, Any], jsonContent: Dict[str, Any], styles: Dict[str, Any]) -> None:
|
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:
|
try:
|
||||||
# Get the actual sheet names that were created (keys are lowercase)
|
# Get the actual sheet names that were created (keys are lowercase)
|
||||||
sheetNames = list(sheets.keys())
|
sheetNames = list(sheets.keys())
|
||||||
|
|
@ -657,7 +700,7 @@ class RendererXlsx(BaseRenderer):
|
||||||
|
|
||||||
sections = self._extractSections(jsonContent)
|
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
|
currentSheetIndex = 0
|
||||||
currentSheet = None
|
currentSheet = None
|
||||||
currentRow = 1
|
currentRow = 1
|
||||||
|
|
@ -665,8 +708,19 @@ class RendererXlsx(BaseRenderer):
|
||||||
for section in sections:
|
for section in sections:
|
||||||
contentType = section.get("content_type", "paragraph")
|
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":
|
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):
|
if currentSheetIndex < len(sheetNames):
|
||||||
sheetName = sheetNames[currentSheetIndex]
|
sheetName = sheetNames[currentSheetIndex]
|
||||||
currentSheet = sheets[sheetName] # sheets dict uses lowercase keys
|
currentSheet = sheets[sheetName] # sheets dict uses lowercase keys
|
||||||
|
|
@ -695,7 +749,7 @@ class RendererXlsx(BaseRenderer):
|
||||||
sheet['A1'] = sheetTitle
|
sheet['A1'] = sheetTitle
|
||||||
title_style = styles.get("title", {})
|
title_style = styles.get("title", {})
|
||||||
sheet['A1'].font = Font(size=16, bold=True, color=self._getSafeColor(title_style.get("color", "FF1F4E79")))
|
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)
|
# Get table data from elements (canonical JSON format)
|
||||||
elements = section.get("elements", [])
|
elements = section.get("elements", [])
|
||||||
|
|
@ -707,8 +761,13 @@ class RendererXlsx(BaseRenderer):
|
||||||
headers = []
|
headers = []
|
||||||
rows = []
|
rows = []
|
||||||
else:
|
else:
|
||||||
headers = content.get("headers", [])
|
headers = content.get("headers") or []
|
||||||
rows = content.get("rows", [])
|
rows = content.get("rows") or []
|
||||||
|
# Ensure headers and rows are lists
|
||||||
|
if not isinstance(headers, list):
|
||||||
|
headers = []
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
rows = []
|
||||||
else:
|
else:
|
||||||
headers = []
|
headers = []
|
||||||
rows = []
|
rows = []
|
||||||
|
|
@ -770,11 +829,11 @@ class RendererXlsx(BaseRenderer):
|
||||||
try:
|
try:
|
||||||
safe_color = self._getSafeColor(title_style["color"])
|
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'].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:
|
except Exception as font_error:
|
||||||
# Try with a safe color
|
# Try with a safe color
|
||||||
sheet['A1'].font = Font(size=title_style["font_size"], bold=title_style["bold"], color="FF000000")
|
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
|
# Generation info
|
||||||
sheet['A3'] = "Generated:"
|
sheet['A3'] = "Generated:"
|
||||||
|
|
@ -892,6 +951,8 @@ class RendererXlsx(BaseRenderer):
|
||||||
startRow = self._addHeadingToExcel(sheet, element, styles, startRow)
|
startRow = self._addHeadingToExcel(sheet, element, styles, startRow)
|
||||||
elif element_type == "image":
|
elif element_type == "image":
|
||||||
startRow = self._addImageToExcel(sheet, element, styles, startRow)
|
startRow = self._addImageToExcel(sheet, element, styles, startRow)
|
||||||
|
elif element_type == "code_block" or element_type == "code":
|
||||||
|
startRow = self._addCodeBlockToExcel(sheet, element, styles, startRow)
|
||||||
else:
|
else:
|
||||||
# Fallback: if element_type not set, use section_type
|
# Fallback: if element_type not set, use section_type
|
||||||
if section_type == "table":
|
if section_type == "table":
|
||||||
|
|
@ -904,6 +965,8 @@ class RendererXlsx(BaseRenderer):
|
||||||
startRow = self._addHeadingToExcel(sheet, element, styles, startRow)
|
startRow = self._addHeadingToExcel(sheet, element, styles, startRow)
|
||||||
elif section_type == "image":
|
elif section_type == "image":
|
||||||
startRow = self._addImageToExcel(sheet, element, styles, startRow)
|
startRow = self._addImageToExcel(sheet, element, styles, startRow)
|
||||||
|
elif section_type == "code_block" or section_type == "code":
|
||||||
|
startRow = self._addCodeBlockToExcel(sheet, element, styles, startRow)
|
||||||
else:
|
else:
|
||||||
startRow = self._addParagraphToExcel(sheet, element, styles, startRow)
|
startRow = self._addParagraphToExcel(sheet, element, styles, startRow)
|
||||||
|
|
||||||
|
|
@ -943,9 +1006,16 @@ class RendererXlsx(BaseRenderer):
|
||||||
content = element.get("content", {})
|
content = element.get("content", {})
|
||||||
if not isinstance(content, dict):
|
if not isinstance(content, dict):
|
||||||
return startRow
|
return startRow
|
||||||
|
|
||||||
headers = content.get("headers", [])
|
headers = content.get("headers", [])
|
||||||
rows = content.get("rows", [])
|
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:
|
if not headers and not rows:
|
||||||
return startRow
|
return startRow
|
||||||
|
|
||||||
|
|
@ -965,12 +1035,21 @@ class RendererXlsx(BaseRenderer):
|
||||||
sanitized_header = self._sanitizeCellValue(header)
|
sanitized_header = self._sanitizeCellValue(header)
|
||||||
cell = sheet.cell(row=headerRow, column=col, value=sanitized_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
|
# Font styling
|
||||||
cell.font = Font(
|
cell.font = Font(
|
||||||
bold=header_style.get("bold", True),
|
bold=header_style.get("bold", True),
|
||||||
color=self._getSafeColor(header_style.get("text_color", "FF000000"))
|
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
|
# Background color
|
||||||
if header_style.get("background"):
|
if header_style.get("background"):
|
||||||
cell.fill = PatternFill(
|
cell.fill = PatternFill(
|
||||||
|
|
@ -978,15 +1057,27 @@ class RendererXlsx(BaseRenderer):
|
||||||
end_color=self._getSafeColor(header_style["background"]),
|
end_color=self._getSafeColor(header_style["background"]),
|
||||||
fill_type="solid"
|
fill_type="solid"
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass # Continue without background color if it fails
|
||||||
|
|
||||||
|
try:
|
||||||
# Alignment
|
# Alignment
|
||||||
cell.alignment = Alignment(
|
cell.alignment = Alignment(
|
||||||
horizontal=header_style.get("align", "left"),
|
horizontal=self._getSafeAlignment(header_style.get("align", "left")),
|
||||||
vertical="center"
|
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
|
# Border
|
||||||
cell.border = thin_border
|
cell.border = thin_border
|
||||||
|
except Exception:
|
||||||
|
pass # Continue without border if it fails
|
||||||
|
|
||||||
startRow += 1
|
startRow += 1
|
||||||
|
|
||||||
|
|
@ -1005,18 +1096,32 @@ class RendererXlsx(BaseRenderer):
|
||||||
sanitized_value = self._sanitizeCellValue(cell_value)
|
sanitized_value = self._sanitizeCellValue(cell_value)
|
||||||
cell = sheet.cell(row=startRow, column=col, value=sanitized_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
|
# Font styling
|
||||||
if cell_style.get("text_color"):
|
if cell_style.get("text_color"):
|
||||||
cell.font = Font(color=self._getSafeColor(cell_style["text_color"]))
|
cell.font = Font(color=self._getSafeColor(cell_style["text_color"]))
|
||||||
|
except Exception:
|
||||||
|
pass # Continue without font color if it fails
|
||||||
|
|
||||||
|
try:
|
||||||
# Alignment
|
# Alignment
|
||||||
cell.alignment = Alignment(
|
cell.alignment = Alignment(
|
||||||
horizontal=cell_style.get("align", "left"),
|
horizontal=self._getSafeAlignment(cell_style.get("align", "left")),
|
||||||
vertical="center"
|
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
|
# Border
|
||||||
cell.border = thin_border
|
cell.border = thin_border
|
||||||
|
except Exception:
|
||||||
|
pass # Continue without border if it fails
|
||||||
|
|
||||||
startRow += 1
|
startRow += 1
|
||||||
|
|
||||||
|
|
@ -1038,7 +1143,10 @@ class RendererXlsx(BaseRenderer):
|
||||||
content = element.get("content", {})
|
content = element.get("content", {})
|
||||||
if not isinstance(content, dict):
|
if not isinstance(content, dict):
|
||||||
return startRow
|
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", {})
|
list_style = styles.get("bullet_list", {})
|
||||||
for item in list_items:
|
for item in list_items:
|
||||||
|
|
@ -1200,6 +1308,52 @@ class RendererXlsx(BaseRenderer):
|
||||||
errorCell.font = Font(color="FFFF0000", italic=True) # Red color
|
errorCell.font = Font(color="FFFF0000", italic=True) # Red color
|
||||||
return startRow + 1
|
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:
|
def _formatTimestamp(self) -> str:
|
||||||
"""Format current timestamp for document generation."""
|
"""Format current timestamp for document generation."""
|
||||||
return datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
|
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]:
|
async def testAllFormats(self) -> Dict[str, Any]:
|
||||||
"""Test document generation in DOCX, XLSX, PPTX, PDF, and HTML formats."""
|
"""Test document generation in DOCX, XLSX, PPTX, PDF, and HTML formats."""
|
||||||
print("\n" + "="*80)
|
print("\n" + "="*80)
|
||||||
print("TESTING DOCUMENT GENERATION IN DOCX, XLSX, PPTX, PDF, AND HTML FORMATS")
|
print("TESTING DOCUMENT GENERATION IN HTML FORMAT")
|
||||||
print("="*80)
|
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 = {}
|
results = {}
|
||||||
|
|
||||||
for format in formats:
|
for format in formats:
|
||||||
|
|
@ -469,7 +471,7 @@ class DocumentGenerationFormatsTester10:
|
||||||
async def runTest(self):
|
async def runTest(self):
|
||||||
"""Run the complete test."""
|
"""Run the complete test."""
|
||||||
print("\n" + "="*80)
|
print("\n" + "="*80)
|
||||||
print("DOCUMENT GENERATION FORMATS TEST 10 - DOCX, XLSX, PPTX, PDF, HTML")
|
print("DOCUMENT GENERATION FORMATS TEST 10 - HTML ONLY")
|
||||||
print("="*80)
|
print("="*80)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue