Vendor templates and prefer local template assets

This commit is contained in:
2026-04-03 08:44:55 +09:00
parent 81b6289f80
commit adef735228
80 changed files with 5077 additions and 267 deletions

View File

@@ -9,12 +9,16 @@ import sys
from pathlib import Path
DESIGN_AGENT_ROOT = Path(r'D:\ad-hoc\kei\design_agent')
REPO_ROOT = Path(__file__).resolve().parent.parent
LOCAL_TEMPLATES_DIR = REPO_ROOT / 'templates'
if str(DESIGN_AGENT_ROOT) not in sys.path:
sys.path.insert(0, str(DESIGN_AGENT_ROOT))
import src.block_reference as block_reference_module
from src.block_reference import select_and_generate_references
from src.config import settings
from src.content_verifier import generate_with_retry
import src.design_director as design_director_module
import src.html_generator as html_generator
from src.design_director import LAYOUT_PRESETS, select_preset
from src.image_utils import embed_images, get_image_sizes
@@ -31,6 +35,7 @@ from src.pipeline_context import (
Topic,
create_context,
)
import src.renderer as renderer_module
from src.renderer import render_slide_from_html
from src.slide_measurer import capture_slide_screenshot, measure_rendered_heights
@@ -39,6 +44,19 @@ if not hasattr(html_generator, 'SIDEBAR_PROMPT') and hasattr(html_generator, '_L
if not hasattr(html_generator, 'FOOTER_PROMPT') and hasattr(html_generator, '_LEGACY_FOOTER_PROMPT'):
html_generator.FOOTER_PROMPT = html_generator._LEGACY_FOOTER_PROMPT
if LOCAL_TEMPLATES_DIR.exists():
block_reference_module.TEMPLATES_DIR = LOCAL_TEMPLATES_DIR
block_reference_module._jinja_env = None
renderer_module.TEMPLATES_DIR = LOCAL_TEMPLATES_DIR
renderer_module.CATALOG_PATH = LOCAL_TEMPLATES_DIR / 'catalog.yaml'
renderer_module._CATALOG_MAP = None
renderer_module._CATALOG_VARIANT_MAP = None
renderer_module._env = None
if hasattr(design_director_module, '_CATALOG_CACHE'):
design_director_module._CATALOG_CACHE = None
if hasattr(design_director_module, '_BLOCK_IDS_CACHE'):
design_director_module._BLOCK_IDS_CACHE = None
from src.space_allocator import (
ContainerSpec as LegacyContainerSpec,
calculate_container_specs,
@@ -290,127 +308,304 @@ def _extract_multiple_sentences(text: str, keywords: list[str], fallback: str, l
return fallback
def _plain_text(value: str) -> str:
text = value or ''
text = re.sub(r'<br\s*/?>', '\n', text, flags=re.I)
text = re.sub(r'<[^>]+>', ' ', text)
text = text.replace('**', '').replace('*', ' ')
text = text.replace('&lt;', '<').replace('&gt;', '>').replace('&amp;', '&')
text = re.sub(r'!\[[^\]]*\]\([^\)]*\)', ' ', text)
text = re.sub(r'\[[^\]]+\]\([^\)]*\)', ' ', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def _markdown_section(text: str, start_marker: str, end_marker: str | None = None) -> str:
start = text.find(start_marker)
if start == -1:
return ''
chunk = text[start + len(start_marker):]
if end_marker:
end = chunk.find(end_marker)
if end != -1:
chunk = chunk[:end]
return chunk.strip()
def _content_after_frontmatter(raw: str) -> str:
if raw.startswith('---'):
parts = raw.split('---', 2)
if len(parts) == 3:
return parts[2].strip()
return raw
def _content_after_frontmatter(raw: str) -> str:
if raw.startswith('---'):
parts = raw.split('---', 2)
if len(parts) == 3:
return parts[2].strip()
return raw
def _problem_bullets_from_raw(raw: str) -> list[str]:
content = _content_after_frontmatter(raw)
before_sep = content.split('<br/>\n---', 1)[0]
bullets = []
for line in before_sep.splitlines():
stripped = line.strip()
if stripped.startswith('* ') and not stripped.startswith('* **'):
bullets.append(_plain_text(stripped[2:]))
return [b for b in bullets if b]
def _details_blocks(raw: str) -> list[str]:
return re.findall(r'<details>(.*?)</details>', raw, flags=re.S)
def _evidence_bullets_from_raw(raw: str) -> list[str]:
blocks = _details_blocks(raw)
if not blocks:
return []
bullets = []
for line in blocks[0].splitlines():
stripped = line.strip()
if stripped.startswith('* '):
bullets.append(_plain_text(stripped[2:]))
return [b for b in bullets if b]
def _definition_sections_from_raw(raw: str) -> list[dict[str, str]]:
match = re.search(r'##\s*1\.[^\n]*\n(.*?)##\s*2\.', raw, flags=re.S)
block = match.group(1) if match else ''
sections: list[dict[str, str]] = []
current_title = None
current_lines: list[str] = []
for line in block.splitlines():
stripped = line.strip()
if stripped.startswith('* **'):
if current_title and current_lines:
sections.append({'title': _plain_text(current_title), 'body': _plain_text(' '.join(current_lines))})
current_title = stripped[2:]
current_lines = []
elif stripped.startswith('* '):
current_lines.append(stripped[2:])
if current_title and current_lines:
sections.append({'title': _plain_text(current_title), 'body': _plain_text(' '.join(current_lines))})
return sections
def _relation_bullets_from_raw(raw: str) -> list[str]:
start = re.search(r'##\s*2\.[^\n]*\n', raw)
if not start:
return []
block = raw[start.end():].split('<details>', 1)[0]
bullets = []
for line in block.splitlines():
stripped = line.strip()
if stripped.startswith('* '):
content = _plain_text(stripped[2:])
if content and '[??' not in content:
bullets.append(content)
return bullets
def _extract_image_src_from_raw(raw: str) -> str:
m = re.search(r'!\[[^\]]*\]\(([^\)]+)\)', raw)
return m.group(1).strip() if m else ''
def _extract_caption_from_raw(raw: str) -> str:
m = re.search(r'\*\[[^\]]+\][^*]+\*', raw)
if m:
return _plain_text(m.group(0))
return 'relation diagram'
def _parse_comparison_rows_from_raw(raw: str) -> list[tuple[str, str, str]]:
blocks = _details_blocks(raw)
if len(blocks) < 2:
return []
rows: list[tuple[str, str, str]] = []
for line in blocks[1].splitlines():
stripped = line.strip()
if not stripped.startswith('|'):
continue
parts = [p.strip() for p in stripped.strip('|').split('|')]
if len(parts) != 3:
continue
if parts[0].startswith(':---') or parts[1].startswith(':---') or parts[2].startswith('---'):
continue
dx, axis, bim = (_plain_text(parts[0]), _plain_text(parts[1]), _plain_text(parts[2]))
if dx == 'DX' and bim == 'BIM':
continue
rows.append((axis, dx, bim))
return rows
def _conclusion_from_raw(raw: str) -> str:
m = re.search(r':::note\[[^\]]+\](.*?):::', raw, flags=re.S)
block = m.group(1) if m else ''
for line in block.splitlines():
stripped = line.strip()
if stripped.startswith('* '):
return _plain_text(stripped[2:])
return _plain_text(block)
def _relation_visual(image_src: str, caption: str) -> str:
if image_src:
return f'<img src="{image_src}" alt="{caption}" style="width:100%; height:220px; object-fit:contain; border:1px solid #dbeafe; border-radius:12px; background:#f8fafc; padding:10px;" />'
return (
'<div style="width:100%; height:220px; border:1px solid #dbeafe; border-radius:12px; background:#f8fafc; padding:10px; display:flex; align-items:center; justify-content:center;">'
'<svg viewBox="0 0 320 220" width="100%" height="100%" aria-label="relation diagram">'
'<circle cx="150" cy="110" r="82" fill="#1d4ed8" opacity="0.95"></circle>'
'<text x="150" y="102" text-anchor="middle" font-size="12" fill="#dbeafe">Digital</text>'
'<text x="150" y="122" text-anchor="middle" font-size="28" font-weight="800" fill="#ffffff">DX</text>'
'<circle cx="92" cy="130" r="38" fill="#f59e0b" opacity="0.95"></circle>'
'<text x="92" y="136" text-anchor="middle" font-size="18" font-weight="800" fill="#ffffff">BIM</text>'
'<circle cx="205" cy="92" r="34" fill="#06b6d4" opacity="0.95"></circle>'
'<text x="205" y="98" text-anchor="middle" font-size="16" font-weight="800" fill="#ffffff">GIS</text>'
'<circle cx="196" cy="154" r="32" fill="#f97316" opacity="0.95"></circle>'
'<text x="196" y="158" text-anchor="middle" font-size="11" font-weight="800" fill="#ffffff">Digital Twin</text>'
'</svg></div>'
)
def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
title = ctx.analysis.title
raw = ctx.raw_content or ''
problem_topic = _topic(ctx, 1)
evidence_topic = _topic(ctx, 4)
definitions_topic = _topic(ctx, 2)
relation_topic = _topic(ctx, 3)
evidence_topic = _topic(ctx, 4)
comparison_topic = _topic(ctx, 5)
conclusion_topic = _topic(ctx, 6)
dx_topic = _topic(ctx, 2)
problem_text = _trim_visible_copy(_prefer_source_text(problem_topic, '건설산업 디지털 전환 논의에서 DX와 BIM이 혼용되며 BIM 도입을 DX 완성으로 오인하는 문제가 발생하고 있다.'), floor=120, ceiling=260)
relation_source = _prefer_source_text(relation_topic, 'DX와 GIS, BIM, Digital Twin의 관계를 시각적으로 드러낸다.')
relation_text = _trim_visible_copy(relation_source, floor=110, ceiling=180)
gis_line = _trim_visible_copy(_extract_sentence(relation_source, 'GIS', 'GIS는 공간 분석과 위치 기반 정보를 제공한다.'), floor=60, ceiling=140)
bim_line = _trim_visible_copy(_extract_sentence(relation_source, 'BIM', 'BIM은 형상정보와 내용정보를 함께 다루는 핵심 인프라 기술이다.'), floor=60, ceiling=160)
evidence_source = _prefer_source_text(evidence_topic, '정책 문서에서도 DX와 BIM이 혼용되며 이를 바로잡을 필요가 있다.')
evidence_text = _trim_visible_copy(evidence_source, floor=90, ceiling=170)
dx_source = _prefer_source_text(dx_topic, 'DX는 상위 개념이고 BIM은 이를 실행하는 핵심 기술이다.')
dx_text = _trim_visible_copy(dx_source, floor=110, ceiling=220)
compare_source = _prefer_source_text(comparison_topic, '범위·프로세스·성과품·확장성의 4개 비교축으로 DX와 BIM 차이를 짧고 직접적으로 보여준다.')
compare_text = _trim_visible_copy(compare_source, floor=90, ceiling=120)
conclusion_text = _trim_visible_copy(_prefer_source_text(conclusion_topic, '결론: BIM은 건설산업 DX를 수행하는 과정의 가장 기초가 되는 일부분이다.'), floor=70, ceiling=180)
problem_title = problem_topic.title if problem_topic and problem_topic.title else 'DX와 BIM의 혼용 문제'
dx_title = dx_topic.title if dx_topic and dx_topic.title else 'DX의 정의와 위치'
relation_title = relation_topic.title if relation_topic and relation_topic.title else 'BIM과 핵심기술의 관계'
comparison_title = comparison_topic.title if comparison_topic and comparison_topic.title else 'DX와 BIM 비교 핵심 포인트'
evidence_title = evidence_topic.title if evidence_topic and evidence_topic.title else '정책 혼용 사례'
problem_title = problem_topic.title if problem_topic and problem_topic.title else 'Problem'
definitions_title = definitions_topic.title if definitions_topic and definitions_topic.title else 'Definitions'
relation_title = relation_topic.title if relation_topic and relation_topic.title else 'Relationship'
evidence_title = evidence_topic.title if evidence_topic and evidence_topic.title else 'Evidence'
comparison_title = comparison_topic.title if comparison_topic and comparison_topic.title else 'Comparison'
body_html = f"""
<div style="width:100%; height:100%; box-sizing:border-box; font-family:'Segoe UI',sans-serif; color:#0f172a; display:flex; flex-direction:column; gap:6px;">
<div style="background:linear-gradient(135deg,#fff7ed 0%,#ffedd5 100%); border:1px solid #fdba74; border-radius:12px; padding:8px 10px;">
<div style="font-size:11px; font-weight:800; color:#c2410c; margin-bottom:4px;">{problem_title}</div>
<div style="font-size:10px; line-height:1.55; color:#7c2d12;">{problem_text}</div>
</div>
problem_bullets = _problem_bullets_from_raw(raw)[:2]
evidence_bullets = _evidence_bullets_from_raw(raw)[:4]
definition_sections = _definition_sections_from_raw(raw)[:3]
relation_bullets = _relation_bullets_from_raw(raw)[:4]
comparison_rows = _parse_comparison_rows_from_raw(raw)
<div class="relation-diagram-card" style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px 16px; box-sizing:border-box; display:flex; flex-direction:column; gap:6px;">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px;">
<div>
<div style="font-size:12px; font-weight:800; color:#1e40af; margin-bottom:4px;">{dx_title}</div>
<div style="font-size:10px; line-height:1.55; color:#334155;">{dx_text}</div>
</div>
<div style="font-size:10px; color:#166534; background:#dcfce7; border:1px solid #86efac; border-radius:999px; padding:4px 8px; white-space:nowrap;">[그림 1] DX와 핵심기술간 상호관계</div>
</div>
preferred_axes = ['??', '????', '???', '???']
picked_rows = [row for row in comparison_rows if row[0] in preferred_axes]
if len(picked_rows) < 4:
seen = {row[0] for row in picked_rows}
for row in comparison_rows:
if row[0] not in seen:
picked_rows.append(row)
seen.add(row[0])
if len(picked_rows) >= 4:
break
picked_rows = picked_rows[:4]
<div style="display:grid; grid-template-columns:198px 1fr; gap:8px; align-items:start;">
<div style="background:#f8fafc; border:1px solid #dbeafe; border-radius:14px; padding:10px; box-sizing:border-box;">
<div style="display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom:8px;">
<div style="min-width:68px; text-align:center; background:#1d4ed8; color:#ffffff; border-radius:999px; padding:8px 12px; font-size:14px; font-weight:800;">DX</div>
<div style="font-size:14px; color:#94a3b8;">→</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:8px; text-align:center; font-size:10px; font-weight:700;">GIS</div>
<div style="background:#dbeafe; border:2px solid #3b82f6; border-radius:10px; padding:8px; text-align:center; font-size:10px; font-weight:800; color:#1d4ed8;">BIM</div>
<div style="grid-column:1 / span 2; background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:8px; text-align:center; font-size:10px; font-weight:700;">Digital Twin</div>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<div style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:12px; padding:8px 10px;">
<div style="font-size:11px; font-weight:800; color:#0f172a; margin-bottom:4px;">{relation_title}</div>
<div style="font-size:10px; line-height:1.55; color:#334155;">{relation_text}</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:8px 9px;">
<div style="font-size:10px; font-weight:800; color:#0f172a; margin-bottom:3px;">GIS 역할</div>
<div style="font-size:8px; line-height:1.45; color:#475569;">{gis_line}</div>
</div>
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:8px 9px;">
<div style="font-size:10px; font-weight:800; color:#0f172a; margin-bottom:3px;">BIM 역할</div>
<div style="font-size:8px; line-height:1.45; color:#475569;">{bim_line}</div>
</div>
</div>
</div>
</div>
</div>
image_src = _extract_image_src_from_raw(raw)
if image_src and ctx.base_path:
candidate = Path(ctx.base_path) / image_src.lstrip('/\\').replace('/', '\\')
if not candidate.exists():
image_src = ''
else:
image_src = ''
image_caption = _extract_caption_from_raw(raw)
conclusion_text = _conclusion_from_raw(raw)
<div class="comparison-summary-card" style="background:#eff6ff; border:1px solid #bfdbfe; border-radius:12px; padding:8px 9px; box-sizing:border-box; display:grid; grid-template-columns:96px 1fr; gap:7px;">
<div>
<div style="font-size:11px; font-weight:800; color:#1d4ed8; margin-bottom:4px;">{comparison_title}</div>
<div style="font-size:8px; line-height:1.45; color:#475569;">{compare_text}</div>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px; font-size:8px; line-height:1.35; color:#334155;">
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:7px 8px;"><span style="font-weight:800; color:#0f172a;">범위</span><br>DX는 BIM을 포함하는 상위 개념</div>
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:7px 8px;"><span style="font-weight:800; color:#0f172a;">프로세스</span><br>DX는 근본적 개선, BIM은 기존 2D 연장</div>
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:7px 8px;"><span style="font-weight:800; color:#0f172a;">성과품</span><br>DX는 공학 정보 연계, BIM은 3D 모델 중심</div>
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:7px 8px;"><span style="font-weight:800; color:#0f172a;">확장성</span><br>DX는 전 생애주기, BIM은 분야별 단절 위험</div>
</div>
</div>
</div>
""".strip()
intro_len = sum(len(x) for x in problem_bullets + evidence_bullets)
defs_len = sum(len(s['body']) for s in definition_sections)
relation_len = sum(len(x) for x in relation_bullets)
sidebar_width = '34%' if defs_len >= relation_len else '31%'
main_width = '66%' if defs_len >= relation_len else '69%'
relation_visual_height = '210px' if intro_len > 320 else '230px'
sidebar_html = f"""
<div style="width:100%; height:100%; box-sizing:border-box; font-family:'Segoe UI',sans-serif; display:flex; flex-direction:column; gap:6px;">
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:12px; padding:8px 10px;">
<div style="font-size:11px; font-weight:800; color:#1e293b; margin-bottom:8px;">용어 정의</div>
<div style="display:grid; grid-template-columns:72px 1fr; row-gap:8px; column-gap:10px; align-items:start; font-size:9px; line-height:1.5; color:#334155;">
<div style="font-weight:800; color:#0f172a;">건설산업</div>
<div>다양한 기술을 통합해 시설물을 구현하는 종합 산업</div>
<div style="font-weight:800; color:#1d4ed8;">BIM</div>
<div>3차원 모델 기반의 정보관리 도구이자 협업 인프라<br><span style="font-size:8px; color:#64748b;">출처: 국토교통부 BIM 기본지침</span></div>
<div style="font-weight:800; color:#1d4ed8;">DX</div>
<div>디지털 기술 기반으로 업무방식과 가치구조를 전환하는 상위 개념</div>
</div>
</div>
<div style="background:#fff7ed; border:1px solid #fdba74; border-radius:12px; padding:8px 10px; box-sizing:border-box;">
<div style="font-size:11px; font-weight:800; color:#c2410c; margin-bottom:5px;">{evidence_title}</div>
<div style="font-size:10px; line-height:1.55; color:#7c2d12;">{evidence_text}</div>
</div>
</div>
""".strip()
problem_items_html = ''.join(
f'<li style="margin-left:16px; margin-bottom:4px;">{_trim_visible_copy(item, floor=90, ceiling=220)}</li>'
for item in problem_bullets
)
evidence_items_html = ''.join(
f'<li style="margin-left:16px; margin-bottom:4px;">{_trim_visible_copy(item, floor=80, ceiling=180)}</li>'
for item in evidence_bullets
)
relation_items_html = ''.join(
f'<li style="margin-left:16px; margin-bottom:4px;">{_trim_visible_copy(item, floor=80, ceiling=210)}</li>'
for item in relation_bullets
)
footer_html = f"""
<div style="background:linear-gradient(135deg, #006aff 0%, #00aaff 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:52px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">
<div style="font-size:12px; font-weight:800; line-height:1.35;">{conclusion_text}</div>
</div>
""".strip()
definition_cards_html = ''
for section in definition_sections:
definition_cards_html += (
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:10px; padding:10px 12px;">'
f'<div style="font-size:11px; font-weight:800; color:#0f172a; margin-bottom:4px;">{section["title"]}</div>'
f'<div style="font-size:9px; line-height:1.55; color:#334155;">{_trim_visible_copy(section["body"], floor=120, ceiling=250)}</div>'
'</div>'
)
comparison_rows_html = ''
for axis, dx, bim in picked_rows:
comparison_rows_html += (
'<tr>'
f'<td style="border:1px solid #bfdbfe; padding:6px 8px; font-size:8px; line-height:1.4; color:#1e3a8a; width:42%;">{_trim_visible_copy(dx, floor=55, ceiling=120)}</td>'
f'<td style="border:1px solid #bfdbfe; padding:6px 8px; font-size:8px; line-height:1.4; font-weight:800; color:#0f172a; width:16%; text-align:center; background:#eff6ff;">{axis}</td>'
f'<td style="border:1px solid #bfdbfe; padding:6px 8px; font-size:8px; line-height:1.4; color:#334155; width:42%;">{_trim_visible_copy(bim, floor=55, ceiling=120)}</td>'
'</tr>'
)
intro_html = (
'<div style="background:linear-gradient(135deg,#fff7ed 0%,#ffedd5 100%); border:1px solid #fdba74; border-radius:12px; padding:10px 12px; display:grid; grid-template-columns:1fr 1fr; gap:12px;">'
f'<div><div style="font-size:12px; font-weight:800; color:#c2410c; margin-bottom:6px;">{problem_title}</div><ul style="font-size:10px; line-height:1.6; color:#7c2d12; padding-left:0; margin:0; list-style:disc;">{problem_items_html}</ul></div>'
f'<div><div style="font-size:12px; font-weight:800; color:#9a3412; margin-bottom:6px;">{evidence_title}</div><ul style="font-size:9px; line-height:1.55; color:#7c2d12; padding-left:0; margin:0; list-style:disc;">{evidence_items_html}</ul></div>'
'</div>'
)
relation_html = (
f'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:12px 14px; display:grid; grid-template-columns:280px 1fr; gap:12px;">'
'<div style="display:flex; flex-direction:column; gap:6px;">'
f'{_relation_visual(image_src, image_caption).replace("height:220px", f"height:{relation_visual_height}")}'
f'<div style="font-size:9px; line-height:1.4; color:#166534; background:#dcfce7; border:1px solid #86efac; border-radius:999px; padding:4px 8px; text-align:center;">{image_caption}</div>'
'</div>'
'<div style="display:flex; flex-direction:column; gap:8px;">'
f'<div style="font-size:12px; font-weight:800; color:#1e40af;">{relation_title}</div>'
f'<ul style="font-size:10px; line-height:1.6; color:#334155; padding-left:0; margin:0; list-style:disc;">{relation_items_html}</ul>'
'</div>'
'</div>'
)
comparison_html = (
'<div style="background:#eff6ff; border:1px solid #bfdbfe; border-radius:12px; padding:8px 10px;">'
f'<div style="font-size:11px; font-weight:800; color:#1d4ed8; margin-bottom:6px;">{comparison_title}</div>'
f'<table style="width:100%; border-collapse:collapse; table-layout:fixed;">{comparison_rows_html}</table>'
'</div>'
)
body_html = (
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:\'Segoe UI\',sans-serif; color:#0f172a; display:flex; flex-direction:column; gap:8px;">'
f'{intro_html}'
f'{relation_html}'
f'{comparison_html}'
'</div>'
)
sidebar_html = (
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:\'Segoe UI\',sans-serif; display:flex; flex-direction:column; gap:8px;">'
f'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:12px; padding:10px 12px;"><div style="font-size:12px; font-weight:800; color:#1e293b; margin-bottom:8px;">{definitions_title}</div><div style="display:flex; flex-direction:column; gap:8px;">{definition_cards_html}</div></div>'
'</div>'
)
footer_html = (
'<div style="background:linear-gradient(135deg, #006aff 0%, #00aaff 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:52px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">'
f'<div style="font-size:12px; font-weight:800; line-height:1.35;">{conclusion_text}</div>'
'</div>'
)
return {
'body_html': body_html,
'sidebar_html': sidebar_html,
'footer_html': footer_html,
'reasoning': f"stage_2 retry regeneration from rollback plan: {retry_plan.get('rollback_stage', 'stage_2')} with design-domain-guided slide composition",
'reasoning': f"retry regrouping by content importance: intro(problem+evidence), body(relation+comparison), sidebar(definitions), widths {main_width}/{sidebar_width}",
}