Add content-driven layout families for run-002 and run-003

This commit is contained in:
2026-04-06 13:22:51 +09:00
parent 43fa31556f
commit d65e69947f
58 changed files with 1770 additions and 948 deletions

View File

@@ -11,6 +11,8 @@ 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'
LOCAL_COMPONENTS_DIR = REPO_ROOT / 'components'
DX_COMPONENT_FALLBACK = Path(r'D:\ad-hoc\cel\src\components\dx.astro')
if str(DESIGN_AGENT_ROOT) not in sys.path:
sys.path.insert(0, str(DESIGN_AGENT_ROOT))
@@ -585,22 +587,346 @@ def _section_card(title: str, lines: list[str], tone: str = 'blue') -> str:
for item in lines if item
)
return (
f'<div style="background:{bg}; border:1px solid {border}; border-radius:14px; padding:12px 14px;">'
f'<div style="background:{bg}; border:1px solid {border}; border-radius:14px; padding:12px 14px; box-sizing:border-box; height:100%; display:flex; flex-direction:column;">'
f'<div style="font-size:13px; font-weight:900; color:{text}; margin-bottom:8px;">{title}</div>'
f'<ul style="font-size:10.4px; line-height:1.6; color:#334155; padding-left:0; margin:0; list-style:disc;">{items_html}</ul>'
f'<ul style="font-size:10.4px; line-height:1.6; color:#334155; padding-left:0; margin:0; list-style:disc; flex:1;">{items_html}</ul>'
'</div>'
)
def _component_placeholder(title: str, summary: str) -> str:
return (
'<div style="background:#ffffff; border:1px dashed #94a3b8; border-radius:14px; padding:14px;">'
'<div style="background:#ffffff; border:1px dashed #94a3b8; border-radius:14px; padding:14px; box-sizing:border-box; height:100%; display:flex; flex-direction:column;">'
f'<div style="font-size:13px; font-weight:900; color:#334155; margin-bottom:8px;">{title}</div>'
f'<div style="font-size:10.4px; line-height:1.62; color:#475569;">{_trim_visible_copy(summary, floor=240, ceiling=560)}</div>'
f'<div style="font-size:10.4px; line-height:1.62; color:#475569; flex:1;">{_trim_visible_copy(summary, floor=240, ceiling=560)}</div>'
'</div>'
)
def _insert_button_into_card(card_html: str, button_html: str) -> str:
idx = card_html.rfind('</div>')
if idx == -1:
return card_html + button_html
return (
card_html[:idx]
+ f'<div style="margin-top:10px; display:flex; justify-content:flex-end;">{button_html}</div>'
+ card_html[idx:]
)
def _load_dx_effect_cards() -> list[tuple[str, list[str]]]:
candidates = [
LOCAL_COMPONENTS_DIR / 'dx.astro',
DX_COMPONENT_FALLBACK,
]
component_text = ''
for path in candidates:
if path.exists():
component_text = path.read_text(encoding='utf-8-sig')
break
if not component_text:
return []
headers = [
_plain_text(item)
for item in re.findall(r'<th class="stakeholder-header [^"]+">([^<]+)</th>', component_text)
]
if not headers:
return []
cards: dict[str, list[str]] = {header: [] for header in headers}
rows = re.findall(r'<tr(?: class="[^"]+")?>(.*?)</tr>', component_text, flags=re.S)
for row in rows:
cells = re.findall(r'<td[^>]*>(.*?)</td>', row, flags=re.S)
if len(cells) < 4:
continue
category = _plain_text(cells[0]).strip()
for index, header in enumerate(headers):
bullets = re.findall(r'<li[^>]*>(.*?)</li>', cells[index + 1], flags=re.S)
for bullet in bullets:
item = _plain_text(bullet)
if not item:
continue
cards[header].append(f'{category}: {item}' if category else item)
return [(header, values[:3]) for header, values in cards.items() if values]
def _extract_heading_block(raw: str, keyword: str) -> str:
lines = raw.splitlines()
start = None
start_level = 0
for idx, line in enumerate(lines):
stripped = line.lstrip()
if stripped.startswith('#') and keyword in stripped:
start = idx + 1
start_level = len(stripped) - len(stripped.lstrip('#'))
break
if start is None:
return ''
end = len(lines)
for idx in range(start, len(lines)):
stripped = lines[idx].lstrip()
if stripped.startswith('#'):
level = len(stripped) - len(stripped.lstrip('#'))
if level <= start_level:
end = idx
break
return chr(10).join(lines[start:end]).strip()
def _extract_grouped_bullets(block: str, base_indent: int = 0) -> list[dict[str, list[str] | str]]:
groups: list[dict[str, list[str] | str]] = []
current: dict[str, list[str] | str] | None = None
for line in block.splitlines():
if not line.strip():
continue
indent = len(line) - len(line.lstrip(' '))
stripped = line.strip()
group_match = re.match(r'^[-*]\s+\*\*(.+?)\*\*(.*)$', stripped)
if group_match and indent == base_indent:
title = _plain_text(group_match.group(1))
tail = _plain_text(group_match.group(2).lstrip(' :'))
current = {'title': title, 'items': []}
if tail:
current['items'].append(tail)
groups.append(current)
continue
if current and re.match(r'^[-*]\s+', stripped):
item = _plain_text(re.sub(r'^[-*]\s+', '', stripped))
if item:
current['items'].append(item)
return groups
def _flatten_group_items(groups: list[dict[str, list[str] | str]]) -> list[str]:
flattened: list[str] = []
for group in groups:
title = str(group.get('title', '')).strip()
for item in group.get('items', []):
text = _plain_text(str(item))
if text:
flattened.append(f'{title}: {text}' if title else text)
return flattened
def _detect_generic_layout_family(ctx: PipelineContext, raw: str) -> str:
relation_types = {getattr(t, 'relation_type', '') for t in ctx.topics}
if '<DxEffect' in raw or 'stakeholder_effect' in relation_types:
return 'goal-image-stakeholder'
if 'requirements' in relation_types and 'product' in relation_types:
return 'requirements-process-product'
return 'section-stack'
def _build_goal_image_stakeholder_layout(ctx: PipelineContext, raw: str) -> dict:
goal_topic = _topic(ctx, 1)
process_topic = _topic(ctx, 2)
support_topic = _topic(ctx, 3)
conclusion_topic = next((t for t in ctx.topics if getattr(t, 'layer', '') == 'conclusion'), ctx.topics[-1] if ctx.topics else None)
goal_title = goal_topic.title if goal_topic and goal_topic.title else ctx.analysis.title
process_title = process_topic.title if process_topic and process_topic.title else 'Process change'
support_title = support_topic.title if support_topic and support_topic.title else 'Stakeholder effects'
conclusion_text = _prefer_source_text(conclusion_topic, ctx.analysis.core_message if ctx.analysis else '')
goal_groups = _extract_grouped_bullets(_extract_heading_block(raw, goal_title), base_indent=0)[:3]
goal_popup_lines = _flatten_group_items(goal_groups)
process_groups = _extract_grouped_bullets(_extract_heading_block(raw, process_title), base_indent=2) or _extract_grouped_bullets(_extract_heading_block(raw, process_title), base_indent=0)
process_popup_lines = _flatten_group_items(process_groups)
dx_cards = _load_dx_effect_cards()
stakeholder_popup_lines = [f'{title}: {line}' for title, lines in dx_cards for line in lines]
image_src = _extract_image_src_from_raw(raw)
if image_src and ctx.base_path:
candidate = Path(ctx.base_path) / image_src.lstrip('/').lstrip(chr(92)).replace('/', chr(92))
if not candidate.exists():
image_src = ''
else:
image_src = ''
image_caption = _extract_caption_from_raw(raw) or goal_title
goal_sections_html = ''.join(
'<div style="background:#ffffff; border:1px solid #d6e2ef; border-left:6px solid {color}; border-radius:12px; padding:10px 12px;">'
'<div style="font-size:12px; font-weight:900; color:#0f172a; margin-bottom:6px;">{title}</div>'
'<ul style="margin:0; padding-left:16px; font-size:10px; line-height:1.5; color:#334155;">{items}</ul>'
'</div>'.format(
color=color,
title=group['title'],
items=''.join(
f'<li style="margin-bottom:5px;">{_trim_visible_copy(_plain_text(str(item)), floor=110, ceiling=240)}</li>'
for item in group.get('items', [])[:2]
),
)
for group, color in zip(goal_groups, ['#c2410c', '#8b6b2e', '#166534'])
)
goal_popup = _popup_overlay('popup-goal', goal_title, _popup_list_html(goal_popup_lines, floor=220, ceiling=680)) if goal_popup_lines else ''
process_popup = _popup_overlay('popup-process', process_title, _popup_list_html(process_popup_lines, floor=220, ceiling=680)) if process_popup_lines else ''
stakeholder_popup = _popup_overlay('popup-stakeholder', support_title, _popup_list_html(stakeholder_popup_lines, floor=220, ceiling=680)) if stakeholder_popup_lines else ''
goal_card = (
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box;">'
f'<div style="font-size:16px; font-weight:900; color:#0f172a; margin-bottom:10px;">{goal_title}</div>'
'<div style="display:grid; grid-template-columns:1.12fr 0.88fr; gap:12px; align-items:stretch;">'
f'<div style="display:flex; flex-direction:column; gap:10px;">{goal_sections_html}</div>'
'<div style="display:flex; flex-direction:column; gap:8px;">'
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:250px")}'
f'<div style="font-size:9px; line-height:1.3; color:#64748b; text-align:center;">{image_caption}</div>'
'</div></div>'
f'<div style="display:flex; justify-content:flex-end; margin-top:10px;">{_popup_button("popup-goal", "Goal details")}</div>'
'</div>'
)
process_cards_html = ''.join(
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:12px; padding:10px 12px;">'
'<div style="font-size:11px; font-weight:800; color:#1e3a8a; margin-bottom:6px;">{title}</div>'
'<ul style="margin:0; padding-left:16px; font-size:9.8px; line-height:1.5; color:#334155;">{items}</ul>'
'</div>'.format(
title=group['title'],
items=''.join(
f'<li style="margin-bottom:4px;">{_trim_visible_copy(_plain_text(str(item)), floor=110, ceiling=240)}</li>'
for item in group.get('items', [])[:2]
),
)
for group in process_groups[:4]
)
process_card = (
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box; height:100%;">'
f'<div style="font-size:14px; font-weight:900; color:#0f172a; margin-bottom:10px;">{process_title}</div>'
f'<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">{process_cards_html}</div>'
f'<div style="display:flex; justify-content:flex-end; margin-top:10px;">{_popup_button("popup-process", "Process details")}</div>'
'</div>'
)
sidebar_parts = []
if dx_cards:
for idx, (title, lines) in enumerate(dx_cards[:3], start=1):
items_html = ''.join(f'<li style="margin-bottom:4px;">{_trim_visible_copy(line, floor=120, ceiling=240)}</li>' for line in lines[:2])
sidebar_parts.append(
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:14px; padding:12px; display:flex; gap:10px; align-items:flex-start;">'
f'<div style="width:34px; height:34px; border-radius:999px; background:#2563eb; color:#fff; font-size:15px; font-weight:800; display:flex; align-items:center; justify-content:center; flex-shrink:0;">{idx}</div>'
'<div style="flex:1;">'
f'<div style="font-size:12px; font-weight:800; color:#0f172a; margin-bottom:6px;">{title}</div>'
f'<ul style="margin:0; padding-left:16px; font-size:9.5px; line-height:1.45; color:#334155;">{items_html}</ul>'
'</div></div>'
)
if not sidebar_parts:
sidebar_parts.append(_component_placeholder(support_title, _prefer_source_text(support_topic, 'No stakeholder detail available.')))
sidebar_html = (
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; display:flex; flex-direction:column; gap:10px;">'
f'<div style="font-size:12px; font-weight:800; color:#475569; padding:2px 6px;">{support_title}</div>'
+ ''.join(sidebar_parts) +
f'<div style="display:flex; justify-content:flex-end;">{_popup_button("popup-stakeholder", "Stakeholder details")}</div>'
'</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:12px;">'
f'{goal_card}'
f'{process_card}'
f'{goal_popup}{process_popup}{stakeholder_popup}'
'</div>'
)
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{_trim_visible_copy(conclusion_text, floor=120, ceiling=320)}</div>' + '</div>'
return {'body_html': body_html, 'sidebar_html': sidebar_html, 'footer_html': footer_html, 'reasoning': 'goal-image-stakeholder layout selected from document content traits'}
def _build_requirements_process_product_layout(ctx: PipelineContext, raw: str) -> dict:
req_topic = _topic(ctx, 1)
process_topic = _topic(ctx, 2)
product_topic = _topic(ctx, 3)
conclusion_topic = next((t for t in ctx.topics if getattr(t, 'layer', '') == 'conclusion'), ctx.topics[-1] if ctx.topics else None)
req_title = req_topic.title if req_topic and req_topic.title else ctx.analysis.title
process_title = process_topic.title if process_topic and process_topic.title else 'Process change'
product_title = product_topic.title if product_topic and product_topic.title else 'Product change'
conclusion_text = _prefer_source_text(conclusion_topic, ctx.analysis.core_message if ctx.analysis else '')
req_groups = _extract_grouped_bullets(_extract_heading_block(raw, req_title), base_indent=0)[:3]
process_groups = _extract_grouped_bullets(_extract_heading_block(raw, process_title), base_indent=0)[:3]
product_groups = _extract_grouped_bullets(_extract_heading_block(raw, product_title), base_indent=0)[:3]
req_popup = _popup_overlay('popup-req', req_title, _popup_list_html(_flatten_group_items(req_groups), floor=220, ceiling=700))
process_popup = _popup_overlay('popup-process', process_title, _popup_list_html(_flatten_group_items(process_groups), floor=220, ceiling=700))
product_popup = _popup_overlay('popup-product', product_title, _popup_list_html(_flatten_group_items(product_groups), floor=220, ceiling=700))
req_cards = ''.join(
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-top:5px solid {color}; border-radius:12px; padding:12px;">'
'<div style="font-size:12px; font-weight:900; color:#0f172a; margin-bottom:8px;">{title}</div>'
'<ul style="margin:0; padding-left:16px; font-size:9.7px; line-height:1.48; color:#334155;">{items}</ul>'
'</div>'.format(
color=color,
title=group['title'],
items=''.join(
f'<li style="margin-bottom:4px;">{_trim_visible_copy(_plain_text(str(item)), floor=120, ceiling=260)}</li>'
for item in group.get('items', [])[:3]
),
)
for group, color in zip(req_groups, ['#2563eb', '#7c3aed', '#16a34a'])
)
body_top = (
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box;">'
f'<div style="font-size:15px; font-weight:900; color:#0f172a; margin-bottom:10px;">{req_title}</div>'
f'<div style="display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:10px;">{req_cards}</div>'
f'<div style="display:flex; justify-content:flex-end; margin-top:10px;">{_popup_button("popup-req", "Requirements details")}</div>'
'</div>'
)
process_cards = ''.join(
'<div style="background:#f8fafc; border:1px solid #d7e2f0; border-radius:12px; padding:12px;">'
'<div style="font-size:11px; font-weight:800; color:#1e3a8a; margin-bottom:6px;">{title}</div>'
'<ul style="margin:0; padding-left:16px; font-size:9.7px; line-height:1.48; color:#334155;">{items}</ul>'
'</div>'.format(
title=group['title'],
items=''.join(
f'<li style="margin-bottom:4px;">{_trim_visible_copy(_plain_text(str(item)), floor=120, ceiling=260)}</li>'
for item in group.get('items', [])[:2]
),
)
for group in process_groups
)
body_bottom = (
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box; flex:1;">'
f'<div style="font-size:14px; font-weight:900; color:#0f172a; margin-bottom:10px;">{process_title}</div>'
f'<div style="display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:10px;">{process_cards}</div>'
f'<div style="display:flex; justify-content:flex-end; margin-top:10px;">{_popup_button("popup-process", "Process details")}</div>'
'</div>'
)
product_cards = ''.join(
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:12px; padding:12px;">'
'<div style="font-size:11px; font-weight:800; color:#0f172a; margin-bottom:6px;">{title}</div>'
'<ul style="margin:0; padding-left:16px; font-size:9.7px; line-height:1.48; color:#334155;">{items}</ul>'
'</div>'.format(
title=group['title'],
items=''.join(
f'<li style="margin-bottom:4px;">{_trim_visible_copy(_plain_text(str(item)), floor=120, ceiling=260)}</li>'
for item in group.get('items', [])[:2]
),
)
for group in product_groups
)
sidebar_html = (
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; display:flex; flex-direction:column; gap:10px;">'
f'<div style="font-size:12px; font-weight:800; color:#475569; padding:2px 6px;">{product_title}</div>'
f'{product_cards}'
f'<div style="display:flex; justify-content:flex-end;">{_popup_button("popup-product", "Product details")}</div>'
'</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:12px;">'
f'{body_top}'
f'{body_bottom}'
f'{req_popup}{process_popup}{product_popup}'
'</div>'
)
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{_trim_visible_copy(conclusion_text, floor=120, ceiling=320)}</div>' + '</div>'
return {'body_html': body_html, 'sidebar_html': sidebar_html, 'footer_html': footer_html, 'reasoning': 'requirements-process-product layout selected from document content traits'}
def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
raw = ctx.raw_content or ''
is_run001_style = _is_run001_style_document(ctx, raw)
@@ -725,6 +1051,12 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{conclusion_text}</div>' + '</div>'
return {'body_html': body_html, 'sidebar_html': sidebar_html, 'footer_html': footer_html, 'reasoning': 'retry regrouping by content importance: grouped problem+evidence with popup details, relation block, visible comparison summary with full popup, numbered definition cards'}
layout_family = _detect_generic_layout_family(ctx, raw)
if layout_family == 'goal-image-stakeholder':
return _build_goal_image_stakeholder_layout(ctx, raw)
if layout_family == 'requirements-process-product':
return _build_requirements_process_product_layout(ctx, raw)
main_topics = [t for t in ctx.topics if getattr(t, 'layer', '') != 'conclusion']
intro_topic = main_topics[0] if len(main_topics) > 0 else None
body_topic = main_topics[1] if len(main_topics) > 1 else None
@@ -740,9 +1072,15 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
body_full = _bulletish_lines(_prefer_source_text(body_topic, ''), 14)
support_full = _bulletish_lines(_prefer_source_text(support_topic, ''), 12)
intro_visible = intro_full[:4]
body_visible = body_full[:4]
support_visible = support_full[:3]
dx_cards = _load_dx_effect_cards() if '<DxEffect' in raw else []
if dx_cards and len(support_full) < 3:
support_full = [f'{title}: {lines[0]}' for title, lines in dx_cards if lines]
intro_visible = intro_full[:5]
body_visible = body_full[:5]
support_visible = support_full[:4]
intro_extra = intro_full[len(intro_visible):len(intro_visible) + 3]
body_extra = body_full[len(body_visible):len(body_visible) + 3]
intro_popup = _popup_overlay('popup-intro', intro_title, _popup_list_html(intro_full, floor=220, ceiling=640)) if len(intro_full) > len(intro_visible) else ''
body_popup = _popup_overlay('popup-body', body_title, _popup_list_html(body_full, floor=220, ceiling=680)) if len(body_full) > len(body_visible) else ''
@@ -759,34 +1097,57 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
intro_card = _section_card(intro_title, intro_visible, tone='orange')
if len(intro_full) > len(intro_visible):
intro_card += _popup_button('popup-intro', '나머지 내용 보기')
intro_card = _insert_button_into_card(intro_card, _popup_button('popup-intro', '나머지 내용 보기'))
body_card = _section_card(body_title, body_visible, tone='blue')
if len(body_full) > len(body_visible):
body_card += _popup_button('popup-body', '상세 본문 보기')
body_card = _insert_button_into_card(body_card, _popup_button('popup-body', '상세 본문 보기'))
if image_src:
support_items_html = ''.join(
f'<li style="margin-left:16px; margin-bottom:6px;">{_trim_visible_copy(item, floor=160, ceiling=360)}</li>'
for item in support_visible
)
visual_block = (
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:14px; padding:12px;">'
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:215px")}'
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-radius:14px; padding:12px; box-sizing:border-box; height:100%; display:flex; flex-direction:column;">'
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:176px")}'
f'<div style="margin-top:8px; font-size:9px; color:#166534; text-align:center;">{image_caption}</div>'
f'<ul style="font-size:10px; line-height:1.55; color:#334155; padding-left:0; margin:10px 0 0 0; list-style:disc; flex:1;">{support_items_html}</ul>'
'</div>'
)
elif support_topic and '<DxEffect' in (support_topic.source_data or support_topic.summary or ''):
visual_block = _component_placeholder(support_title, '주체별 기대효과 도식은 요약 카드와 팝업으로 함께 제공함.')
elif dx_cards:
summary_lines = [f'{title}: {lines[0]}' for title, lines in dx_cards if lines][:4]
visual_block = _section_card(support_title, summary_lines, tone='slate')
visual_block = _insert_button_into_card(visual_block, _popup_button('popup-support', '주체별 상세 보기'))
else:
visual_block = _section_card(support_title, support_visible, tone='slate')
if len(support_full) > len(support_visible):
visual_block += _popup_button('popup-support', '상세 보조 내용 보기')
visual_block = _insert_button_into_card(visual_block, _popup_button('popup-support', '상세 보조 내용 보기'))
sidebar_inner = _section_card(support_title, support_visible, tone='slate') if support_visible else _component_placeholder(support_title, _prefer_source_text(support_topic, '보조 정보가 없음.'))
if len(support_full) > len(support_visible):
sidebar_inner += _popup_button('popup-support', '상세 내용 보기')
sidebar_parts: list[str] = []
if dx_cards:
for title, lines in dx_cards[:3]:
sidebar_parts.append(_section_card(title, lines[:3], tone='slate'))
else:
if intro_extra:
sidebar_parts.append(_section_card(intro_title, intro_extra, tone='orange'))
if body_extra:
sidebar_parts.append(_section_card(body_title, body_extra, tone='blue'))
if support_visible:
support_sidebar = _section_card(support_title, support_visible, tone='slate')
if len(support_full) > len(support_visible):
support_sidebar = _insert_button_into_card(support_sidebar, _popup_button('popup-support', '?? ?? ?? ??'))
sidebar_parts.append(support_sidebar)
if not sidebar_parts:
sidebar_parts.append(_component_placeholder(support_title, _prefer_source_text(support_topic, '보조 정보가 없음.')))
sidebar_inner = ''.join(sidebar_parts)
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:10px;">'
'<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; color:#0f172a; display:flex; flex-direction:column; gap:12px;">'
f'{intro_card}'
'<div style="display:grid; grid-template-columns:1.05fr 0.95fr; gap:12px; align-items:start;">'
'<div style="display:grid; grid-template-columns:1.08fr 0.92fr; gap:12px; align-items:stretch; flex:1; min-height:0;">'
f'{body_card}'
f'{visual_block}'
'</div>'
@@ -794,7 +1155,7 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
'</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:10px;">' + sidebar_inner + '</div>'
sidebar_html = '<div style="width:100%; height:100%; box-sizing:border-box; font-family:Segoe UI,sans-serif; display:grid; grid-auto-rows:minmax(0,1fr); gap:10px;">' + sidebar_inner + '</div>'
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:10px 20px; text-align:center; color:#ffffff; width:100%; height:58px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:13px; font-weight:900; line-height:1.35;">{_trim_visible_copy(conclusion_text, floor=120, ceiling=320)}</div>' + '</div>'
return {'body_html': body_html, 'sidebar_html': sidebar_html, 'footer_html': footer_html, 'reasoning': 'generic retry layout for non-run001 documents: preserve section titles, keep visible summary blocks, and move overflow detail into popups'}