Add content-driven layout families for run-002 and run-003
This commit is contained in:
@@ -60,8 +60,15 @@ def validate_outputs(generated: dict, measurement: dict, required_titles: list[s
|
||||
failures: list[str] = []
|
||||
actions: list[str] = []
|
||||
|
||||
slide_overflow = measurement.get("slide", {}).get("overflowed")
|
||||
slide_info = measurement.get("slide", {})
|
||||
zones_info = measurement.get("zones", {})
|
||||
slide_overflow = slide_info.get("overflowed")
|
||||
zone_overflows = zone_overflow_names(measurement)
|
||||
measurement_missing = not slide_info or not zones_info
|
||||
|
||||
if measurement_missing:
|
||||
failures.append("Verify-Measurement")
|
||||
actions.append("?? ??? ?? ???? stage 3/4 ?? ? ?? ??? ?? ???? ?? ??? ?? ????.")
|
||||
|
||||
if slide_overflow:
|
||||
failures.append("Verify-RenderSlide")
|
||||
|
||||
@@ -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'}
|
||||
|
||||
Reference in New Issue
Block a user