Checkpoint raw-preservation pipeline and Type B refactor
This commit is contained in:
@@ -14,6 +14,7 @@ if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.gitea_issue_sync import create_comment
|
||||
from scripts.raw_bootstrap import rebuild_run_from_raw
|
||||
|
||||
DESIGN_AGENT_ROOT = Path(r"D:\ad-hoc\kei\design_agent")
|
||||
if str(DESIGN_AGENT_ROOT) not in sys.path:
|
||||
@@ -387,6 +388,8 @@ def main() -> None:
|
||||
comments_dir.mkdir(parents=True, exist_ok=True)
|
||||
validation_path = run_dir / "06-validation" / "validation-result.md"
|
||||
|
||||
bootstrap_info = rebuild_run_from_raw(repo_root, run_dir, input_file)
|
||||
|
||||
issue_numbers = [int(x.strip()) for x in args.issue_numbers.split(",")]
|
||||
step_comment_bodies = {
|
||||
1: comments_dir / "step-1.md",
|
||||
|
||||
388
scripts/raw_bootstrap.py
Normal file
388
scripts/raw_bootstrap.py
Normal file
@@ -0,0 +1,388 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _read_text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8-sig")
|
||||
|
||||
|
||||
def _write_json(path: Path, data: dict[str, Any]) -> None:
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _write_text(path: Path, text: str) -> None:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def _compact(text: str, max_len: int) -> str:
|
||||
normalized = re.sub(r"\s+", " ", text).strip()
|
||||
if len(normalized) <= max_len:
|
||||
return normalized
|
||||
cut = normalized[:max_len].rsplit(" ", 1)[0].strip()
|
||||
return (cut or normalized[:max_len]).rstrip(" ,.;:") + "..."
|
||||
|
||||
|
||||
def _preserve_len(text: str, ratio: float = 0.85, floor: int = 180, ceiling: int = 900) -> int:
|
||||
normalized = re.sub(r"\s+", " ", text).strip()
|
||||
if not normalized:
|
||||
return floor
|
||||
return max(floor, min(ceiling, int(len(normalized) * ratio)))
|
||||
|
||||
|
||||
def _strip_frontmatter_and_imports(raw: str) -> str:
|
||||
text = raw.replace("\r\n", "\n")
|
||||
if text.startswith("---\n"):
|
||||
end = text.find("\n---", 4)
|
||||
if end != -1:
|
||||
text = text[end + 4 :]
|
||||
text = re.sub(r"^import\s+.+?$", "", text, flags=re.M)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _dx_effect_lines(repo_root: Path) -> list[str]:
|
||||
path = repo_root / "components" / "dx.astro"
|
||||
if not path.exists():
|
||||
return []
|
||||
text = _read_text(path)
|
||||
text = re.sub(r"<style.*?</style>", "", text, flags=re.S)
|
||||
text = text.replace("<br />", " ")
|
||||
text = re.sub(r"</?(div|table|thead|tbody|tr|td|th|colgroup|col|ul|strong)[^>]*>", "\n", text)
|
||||
text = re.sub(r"<li[^>]*>", "- ", text)
|
||||
text = re.sub(r"</li>", "\n", text)
|
||||
text = re.sub(r"<[^>]+>", " ", text)
|
||||
lines: list[str] = []
|
||||
for raw in text.splitlines():
|
||||
line = re.sub(r"\s+", " ", raw).strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("/*") or line.startswith("["):
|
||||
continue
|
||||
if len(line) < 6:
|
||||
continue
|
||||
lines.append(line)
|
||||
deduped: list[str] = []
|
||||
for line in lines:
|
||||
if line not in deduped:
|
||||
deduped.append(line)
|
||||
return deduped[:24]
|
||||
|
||||
|
||||
def _normalize_block_for_storage(text: str, repo_root: Path) -> str:
|
||||
dx_lines = _dx_effect_lines(repo_root)
|
||||
if "<DxEffect" in text and dx_lines:
|
||||
replacement = "\n".join(f"* {line}" for line in dx_lines)
|
||||
text = re.sub(r"<DxEffect\s*/>", replacement, text)
|
||||
text = re.sub(r"<summary[^>]*>(.*?)</summary>", lambda m: f"**{re.sub(r'<[^>]+>', ' ', m.group(1)).strip()}**", text, flags=re.S)
|
||||
text = text.replace("<details>", "").replace("</details>", "")
|
||||
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.I)
|
||||
text = re.sub(r"</?div[^>]*>", "", text)
|
||||
text = re.sub(r":::\s*note\[(.*?)\]", r"**\1**", text)
|
||||
text = text.replace(":::", "")
|
||||
text = re.sub(r"!\[([^\]]+)\]\(([^\)]+)\)", r"[???] \1", text)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _first_nonempty_lines(text: str, limit: int = 8) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for raw in text.splitlines():
|
||||
line = raw.strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("---"):
|
||||
continue
|
||||
lines.append(line)
|
||||
if len(lines) >= limit:
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def _extract_detail_topics(block: str, start_id: int, repo_root: Path) -> tuple[list[dict[str, Any]], str, int]:
|
||||
topics: list[dict[str, Any]] = []
|
||||
next_id = start_id
|
||||
|
||||
def repl(match: re.Match[str]) -> str:
|
||||
nonlocal next_id
|
||||
inner = match.group(1)
|
||||
summary_match = re.search(r"<summary[^>]*>(.*?)</summary>", inner, flags=re.S)
|
||||
summary = re.sub(r"<[^>]+>", " ", summary_match.group(1)).strip() if summary_match else "?? ??"
|
||||
detail_body = re.sub(r"<summary[^>]*>.*?</summary>", "", inner, flags=re.S)
|
||||
detail_source = _normalize_block_for_storage(detail_body, repo_root)
|
||||
if detail_source:
|
||||
topics.append({
|
||||
"id": next_id,
|
||||
"title": summary,
|
||||
"purpose": "?? ?? ??",
|
||||
"role": "reference",
|
||||
"layer": "supporting",
|
||||
"source_hint": summary,
|
||||
"summary": _compact(detail_source, _preserve_len(detail_source, floor=220, ceiling=560)),
|
||||
"source_data": detail_source,
|
||||
})
|
||||
next_id += 1
|
||||
return f"\n* **{summary}**\n"
|
||||
|
||||
stripped = re.sub(r"<details>(.*?)</details>", repl, block, flags=re.S)
|
||||
return topics, stripped, next_id
|
||||
|
||||
|
||||
def _extract_title_from_intro(block: str) -> str:
|
||||
m = re.search(r"\*\s+\*\*(.+?)\*\*", block)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return "도입"
|
||||
|
||||
|
||||
def _section_chunks(text: str) -> list[tuple[str, str]]:
|
||||
matches = list(re.finditer(r"^##\s+(.+)$", text, flags=re.M))
|
||||
chunks: list[tuple[str, str]] = []
|
||||
for idx, match in enumerate(matches):
|
||||
title = match.group(1).strip()
|
||||
start = match.end()
|
||||
end = matches[idx + 1].start() if idx + 1 < len(matches) else len(text)
|
||||
chunks.append((title, text[start:end].strip()))
|
||||
return chunks
|
||||
|
||||
|
||||
def _subsection_chunks(text: str) -> list[tuple[str, str]]:
|
||||
matches = list(re.finditer(r"^###\s+(.+)$", text, flags=re.M))
|
||||
chunks: list[tuple[str, str]] = []
|
||||
for idx, match in enumerate(matches):
|
||||
title = match.group(1).strip()
|
||||
start = match.end()
|
||||
end = matches[idx + 1].start() if idx + 1 < len(matches) else len(text)
|
||||
chunks.append((title, text[start:end].strip()))
|
||||
return chunks
|
||||
|
||||
|
||||
def _classify(title: str, layer_hint: str = "core") -> tuple[str, str, str]:
|
||||
clean = title.strip()
|
||||
if "혼용" in clean:
|
||||
return "problem", "flow", "intro"
|
||||
if "정의" in clean:
|
||||
return "definition", "flow", "core"
|
||||
if "상호관계" in clean or "관계" in clean:
|
||||
return "hierarchy", "flow", "core"
|
||||
if "구분" in clean or "비교" in clean:
|
||||
return "comparison", "reference", "supporting"
|
||||
if "사례" in clean:
|
||||
return "evidence", "reference", "supporting"
|
||||
if "궁극적 목표" in clean:
|
||||
return "goal", "flow", "core"
|
||||
if "기대효과" in clean:
|
||||
return "stakeholder_effect", "flow", "core"
|
||||
if "필수 요건" in clean:
|
||||
return "requirements", "flow", "core"
|
||||
if "Process" in clean or "과정" in clean:
|
||||
return "process", "flow", "core"
|
||||
if "Product" in clean or "결과" in clean:
|
||||
return "product", "flow", "core"
|
||||
if "핵심 요약" in clean or "결론" in clean:
|
||||
return "conclusion", "flow", "conclusion"
|
||||
if layer_hint == "supporting":
|
||||
return "support", "reference", "supporting"
|
||||
return "section", "flow", "core"
|
||||
|
||||
|
||||
def _extract_conclusion(text: str, repo_root: Path) -> tuple[str, str]:
|
||||
m = re.search(r":::\s*note\[(.*?)\](.*?):::", text, flags=re.S)
|
||||
if not m:
|
||||
return text, ""
|
||||
note_title = re.sub(r"\s+", " ", m.group(1)).strip() or "\ud575\uc2ec \uc694\uc57d"
|
||||
note_body = _normalize_block_for_storage(m.group(2), repo_root)
|
||||
note_source = f"**{note_title}**\n{note_body}".strip()
|
||||
stripped = text[: m.start()] + text[m.end() :]
|
||||
return stripped.strip(), note_source
|
||||
|
||||
|
||||
def extract_topics_from_raw(raw: str, repo_root: Path) -> tuple[str, list[dict[str, Any]]]:
|
||||
title_match = re.search(r"^title:\s*(.+)$", raw, flags=re.M)
|
||||
doc_title = title_match.group(1).strip() if title_match else "Document"
|
||||
clean = _strip_frontmatter_and_imports(raw)
|
||||
clean, conclusion_source = _extract_conclusion(clean, repo_root)
|
||||
|
||||
topics: list[dict[str, Any]] = []
|
||||
next_id = 1
|
||||
|
||||
first_section = re.search(r"^##\s+", clean, flags=re.M)
|
||||
intro_block = clean[: first_section.start()].strip() if first_section else clean.strip()
|
||||
if intro_block:
|
||||
detail_topics, intro_stripped, _ = _extract_detail_topics(intro_block, next_id + 1, repo_root)
|
||||
intro_source = _normalize_block_for_storage(intro_stripped, repo_root)
|
||||
if intro_source:
|
||||
title = _extract_title_from_intro(intro_source)
|
||||
relation, role, layer = _classify(title, "intro")
|
||||
topics.append({
|
||||
"id": next_id,
|
||||
"title": title,
|
||||
"purpose": "?? ?? ?? ??",
|
||||
"role": role,
|
||||
"layer": layer,
|
||||
"source_hint": title,
|
||||
"summary": _compact(intro_source, _preserve_len(intro_source, floor=260, ceiling=760)),
|
||||
"source_data": intro_source,
|
||||
})
|
||||
next_id += 1
|
||||
topics.extend(detail_topics)
|
||||
next_id = max([t["id"] for t in topics], default=0) + 1
|
||||
|
||||
for section_title, section_body in _section_chunks(clean):
|
||||
detail_topics, section_stripped, next_id = _extract_detail_topics(section_body, next_id, repo_root)
|
||||
subsections = _subsection_chunks(section_stripped)
|
||||
lead = re.split(r"^###\s+.+$", section_stripped, maxsplit=1, flags=re.M)[0].strip() if subsections else section_stripped
|
||||
if lead:
|
||||
source = _normalize_block_for_storage(lead, repo_root)
|
||||
if source:
|
||||
relation, role, layer = _classify(section_title)
|
||||
topics.append({
|
||||
"id": next_id,
|
||||
"title": section_title,
|
||||
"purpose": f"{section_title} ?? ??",
|
||||
"role": role,
|
||||
"layer": layer,
|
||||
"source_hint": section_title,
|
||||
"summary": _compact(source, _preserve_len(source, floor=240, ceiling=780)),
|
||||
"source_data": source,
|
||||
})
|
||||
next_id += 1
|
||||
for sub_title, sub_body in subsections:
|
||||
source = _normalize_block_for_storage(sub_body, repo_root)
|
||||
if source:
|
||||
relation, role, layer = _classify(sub_title)
|
||||
topics.append({
|
||||
"id": next_id,
|
||||
"title": sub_title,
|
||||
"purpose": f"{sub_title} ?? ??",
|
||||
"role": role,
|
||||
"layer": layer,
|
||||
"source_hint": sub_title,
|
||||
"summary": _compact(source, _preserve_len(source, floor=220, ceiling=760)),
|
||||
"source_data": source,
|
||||
})
|
||||
next_id += 1
|
||||
topics.extend(detail_topics)
|
||||
next_id = max([t["id"] for t in topics], default=0) + 1
|
||||
|
||||
if conclusion_source:
|
||||
topics.append({
|
||||
"id": next_id,
|
||||
"title": "\ud575\uc2ec \uc694\uc57d",
|
||||
"purpose": "?? ?? ??",
|
||||
"role": "flow",
|
||||
"layer": "conclusion",
|
||||
"source_hint": "\ud575\uc2ec \uc694\uc57d",
|
||||
"summary": _compact(conclusion_source, _preserve_len(conclusion_source, floor=140, ceiling=360)),
|
||||
"source_data": conclusion_source,
|
||||
})
|
||||
|
||||
return doc_title, topics
|
||||
|
||||
|
||||
def _page_structure(topics: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
intro_ids = [t["id"] for t in topics if t["layer"] == "intro"]
|
||||
core_ids = [t["id"] for t in topics if t["layer"] == "core"]
|
||||
support_ids = [t["id"] for t in topics if t["layer"] == "supporting"]
|
||||
conclusion_ids = [t["id"] for t in topics if t["layer"] == "conclusion"]
|
||||
structure: dict[str, Any] = {}
|
||||
if intro_ids:
|
||||
structure["background"] = {"topic_ids": intro_ids, "weight": 0.24}
|
||||
if core_ids:
|
||||
structure["body"] = {"topic_ids": core_ids, "weight": 0.48 if support_ids else 0.58}
|
||||
if support_ids:
|
||||
structure["support"] = {"topic_ids": support_ids, "weight": 0.18}
|
||||
if conclusion_ids:
|
||||
structure["key_message"] = {"topic_ids": conclusion_ids, "weight": 0.10}
|
||||
return structure
|
||||
|
||||
|
||||
def rebuild_run_from_raw(repo_root: Path, run_dir: Path, input_file: Path) -> dict[str, Any]:
|
||||
raw = _read_text(input_file)
|
||||
doc_title, topics = extract_topics_from_raw(raw, repo_root)
|
||||
core_topic = next((t for t in topics if t["layer"] == "conclusion"), topics[-1] if topics else {"source_data": ""})
|
||||
stage1a = {
|
||||
"analysis": {
|
||||
"title": doc_title,
|
||||
"core_message": re.sub(r"\s+", " ", str(core_topic.get("source_data", ""))).strip(),
|
||||
"total_pages": 1,
|
||||
},
|
||||
"page_structure": _page_structure(topics),
|
||||
"topics": topics,
|
||||
}
|
||||
stage1b = {
|
||||
"concepts": [
|
||||
{
|
||||
"topic_id": t["id"],
|
||||
"relation_type": _classify(t["title"], t["layer"])[0],
|
||||
"expression_hint": "?? ??? ??? ???. ??? ? ?? ??? ??? popup?? ???. visible ??? ?? ???? 85% ??? ?? ???.",
|
||||
"summary": t["summary"],
|
||||
}
|
||||
for t in topics
|
||||
]
|
||||
}
|
||||
|
||||
plan_dir = run_dir / "04-plan"
|
||||
plan_dir.mkdir(parents=True, exist_ok=True)
|
||||
_write_json(plan_dir / "stage-1a-topics.json", stage1a)
|
||||
_write_json(plan_dir / "stage-1b-refined-concepts.json", stage1b)
|
||||
|
||||
input_dir = run_dir / "01-input"
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
input_lines = [
|
||||
"# Input Review",
|
||||
"",
|
||||
f"- ?? ???: {input_file.name}",
|
||||
f"- ?? ??: {doc_title}",
|
||||
"- ?? ?? ??: ?? block? ???? ?? ???? ???.",
|
||||
"- ?? ??: ???? ?? 85% ?? ????, ? ?/?? ??? popup ??? ???.",
|
||||
"",
|
||||
"## ?? ??",
|
||||
]
|
||||
for topic in topics:
|
||||
input_lines.append(f"- {topic['title']}: { _compact(re.sub(r'\s+', ' ', topic['source_data']), 160) }")
|
||||
_write_text(input_dir / "input-review.md", "\n".join(input_lines) + "\n")
|
||||
|
||||
interp_dir = run_dir / "02-kei-interpretation"
|
||||
interp_dir.mkdir(parents=True, exist_ok=True)
|
||||
interp_lines = [
|
||||
"# Interpretation",
|
||||
"",
|
||||
"- ?? ??: ????? ?? ??? ???.",
|
||||
"- ?? ??: ?? ??? ????, ??/??/popup ???? ???.",
|
||||
"- popup ??: ? ?, ?? ??, ? ??? ??? popup?? ?? ???.",
|
||||
"",
|
||||
"## Topic Classification",
|
||||
]
|
||||
for topic in topics:
|
||||
interp_lines.append(f"- {topic['title']}: layer={topic['layer']} / role={topic['role']}")
|
||||
_write_text(interp_dir / "kei-interpretation.md", "\n".join(interp_lines) + "\n")
|
||||
|
||||
structure_dir = run_dir / "03-structure"
|
||||
structure_dir.mkdir(parents=True, exist_ok=True)
|
||||
structure_lines = [
|
||||
"# Content Structure",
|
||||
"",
|
||||
"- ??? ??: ?? ?? ??? ???.",
|
||||
"- ??? ??: ?? ? ???? ????, ?? ???? ????.",
|
||||
"- popup ??: ??? ? ?? ??? ? ?/? ??? popup?? ???.",
|
||||
"",
|
||||
"## Ordered Blocks",
|
||||
]
|
||||
for idx, topic in enumerate(topics, start=1):
|
||||
structure_lines.append(f"{idx}. {topic['title']} ({topic['layer']})")
|
||||
_write_text(structure_dir / "content-structure.md", "\n".join(structure_lines) + "\n")
|
||||
|
||||
plan_lines = [
|
||||
"# Execution Plan",
|
||||
"",
|
||||
"- ??? raw mdx?? ?? ???? stage-1a/stage-1b? ???.",
|
||||
"- ?? ??? ??? ???.",
|
||||
"- ?? ??, ? ?, ??? ?? ??? popup?? ?? ???.",
|
||||
"- visible ??? section title + ?? bullet + ?? ?? ???? ???.",
|
||||
]
|
||||
_write_text(plan_dir / "execution-plan.md", "\n".join(plan_lines) + "\n")
|
||||
|
||||
return {"title": doc_title, "topics": topics}
|
||||
@@ -1,4 +1,4 @@
|
||||
from __future__ import annotations
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
@@ -291,17 +291,15 @@ def _prefer_source_text(topic: Topic | None, fallback: str) -> str:
|
||||
if not topic:
|
||||
return fallback
|
||||
source = re.sub(r"\s+", " ", (topic.source_data or "")).strip()
|
||||
if source and len(source) >= max(80, len(fallback)):
|
||||
if source:
|
||||
return source
|
||||
summary = re.sub(r"\s+", " ", (topic.summary or "")).strip()
|
||||
if source and len(source) >= 40:
|
||||
return source
|
||||
if summary:
|
||||
return summary
|
||||
return fallback
|
||||
|
||||
|
||||
def _trim_visible_copy(text: str, floor: int = 120, ceiling: int = 320) -> str:
|
||||
def _trim_visible_copy(text: str, floor: int = 180, ceiling: int = 520) -> str:
|
||||
normalized = re.sub(r"\s+", " ", text).strip()
|
||||
if not normalized:
|
||||
return ""
|
||||
@@ -413,9 +411,9 @@ def _details_blocks(raw: str) -> list[str]:
|
||||
|
||||
def _popup_list_html(items: list[str], floor: int = 90, ceiling: int = 260) -> str:
|
||||
if not items:
|
||||
return '<div style="font-size:12px; color:#64748b;">??? ?? ??? ????.</div>'
|
||||
return '<div style="font-size:12px; color:#64748b;">?Details ?Details??.</div>'
|
||||
lis = ''.join(
|
||||
f'<li style="margin-left:18px; margin-bottom:8px; line-height:1.55;">{_trim_visible_copy(item, floor=floor, ceiling=ceiling)}</li>'
|
||||
f'<li style="margin-left:18px; margin-bottom:8px; line-height:1.62;">{_trim_visible_copy(item, floor=max(floor, min(len(re.sub(r"\s+", " ", item).strip()), 260)), ceiling=max(ceiling, 860))}</li>'
|
||||
for item in items
|
||||
)
|
||||
return f'<ul style="margin:0; padding-left:0; list-style:disc; font-size:13px; color:#334155;">{lis}</ul>'
|
||||
@@ -423,12 +421,12 @@ def _popup_list_html(items: list[str], floor: int = 90, ceiling: int = 260) -> s
|
||||
|
||||
def _popup_comparison_table(rows: list[tuple[str, str, str]]) -> str:
|
||||
if not rows:
|
||||
return '<div style="font-size:12px; color:#64748b;">??? ???? ????.</div>'
|
||||
return '<div style="font-size:12px; color:#64748b;">?DetailsDetails??.</div>'
|
||||
body = ''.join(
|
||||
'<tr>'
|
||||
f'<td style="padding:10px 12px; border-top:1px solid #e2e8f0; font-size:12px; line-height:1.5; color:#1e3a8a; vertical-align:top;">{_trim_visible_copy(dx, floor=160, ceiling=420)}</td>'
|
||||
f'<td style="padding:10px 12px; border-top:1px solid #e2e8f0; font-size:12px; line-height:1.58; color:#1e3a8a; vertical-align:top;">{_trim_visible_copy(dx, floor=220, ceiling=960)}</td>'
|
||||
f'<td style="padding:10px 8px; border-top:1px solid #e2e8f0; font-size:12px; line-height:1.35; color:#1d4ed8; font-weight:800; background:#eff6ff; text-align:center; vertical-align:top;">{axis}</td>'
|
||||
f'<td style="padding:10px 12px; border-top:1px solid #e2e8f0; font-size:12px; line-height:1.5; color:#475569; vertical-align:top;">{_trim_visible_copy(bim, floor=160, ceiling=420)}</td>'
|
||||
f'<td style="padding:10px 12px; border-top:1px solid #e2e8f0; font-size:12px; line-height:1.58; color:#475569; vertical-align:top;">{_trim_visible_copy(bim, floor=220, ceiling=960)}</td>'
|
||||
'</tr>'
|
||||
for axis, dx, bim in rows
|
||||
)
|
||||
@@ -618,7 +616,7 @@ def _component_placeholder(title: str, summary: str) -> str:
|
||||
def _type_b_body_shell(inner_html: str) -> str:
|
||||
return (
|
||||
'<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; position:relative; z-index:2;">'
|
||||
'display:flex; flex-direction:column; gap:8px; position:relative; z-index:2;">'
|
||||
f'{inner_html}'
|
||||
'</div>'
|
||||
)
|
||||
@@ -721,6 +719,87 @@ def _extract_grouped_bullets(block: str, base_indent: int = 0) -> list[dict[str,
|
||||
return groups
|
||||
|
||||
|
||||
def _table_rows_from_block(block: str) -> list[tuple[str, str, str]]:
|
||||
rows: list[tuple[str, str, str]] = []
|
||||
for line in block.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
|
||||
left, axis, right = (_plain_text(parts[0]), _plain_text(parts[1]), _plain_text(parts[2]))
|
||||
if left.startswith('As-is') and right.startswith('To-be'):
|
||||
continue
|
||||
rows.append((left, axis, right))
|
||||
return rows
|
||||
|
||||
|
||||
def _table_rows_as_lines(rows: list[tuple[str, str, str]], limit: int = 4) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for left, axis, right in rows[:limit]:
|
||||
merged = f'{left} ? {right}'
|
||||
if axis:
|
||||
merged = f'{axis}: {merged}'
|
||||
lines.append(merged)
|
||||
return lines
|
||||
|
||||
|
||||
def _section_table_lines(raw: str, title: str, limit: int = 4) -> list[str]:
|
||||
return _table_rows_as_lines(_table_rows_from_block(_extract_heading_block(raw, title)), limit=limit)
|
||||
|
||||
|
||||
def _bullet_lines_from_block(block: str, limit: int = 8) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for raw in block.splitlines():
|
||||
stripped = raw.strip()
|
||||
if not stripped.startswith(('-', '*')):
|
||||
continue
|
||||
cleaned = _plain_text(re.sub(r'^[-*]\s+', '', stripped))
|
||||
if cleaned:
|
||||
lines.append(cleaned)
|
||||
deduped: list[str] = []
|
||||
for line in lines:
|
||||
if line not in deduped:
|
||||
deduped.append(line)
|
||||
return deduped[:limit]
|
||||
|
||||
|
||||
def _group_card(title: str, groups: list[dict[str, list[str] | str]], tone: str = 'blue', max_items_per_group: int = 2, title_font: int = 11, body_font: float = 9.5) -> str:
|
||||
palette = {
|
||||
'blue': ('#eff6ff', '#93c5fd', '#1e3a8a'),
|
||||
'slate': ('#f8fafc', '#cbd5e1', '#334155'),
|
||||
'green': ('#ecfdf5', '#86efac', '#166534'),
|
||||
}
|
||||
bg, border, accent = palette.get(tone, palette['blue'])
|
||||
group_html = ''
|
||||
for group in groups:
|
||||
items = [_plain_text(str(item)) for item in group.get('items', [])][:max_items_per_group]
|
||||
if not items:
|
||||
continue
|
||||
group_html += (
|
||||
f'<div style="margin-bottom:8px;">'
|
||||
f'<div style="font-size:{title_font}px; font-weight:800; color:{accent}; margin-bottom:4px;">{group["title"]}</div>'
|
||||
f'<ul style="margin:0; padding-left:14px; font-size:{body_font}px; line-height:1.42; color:#334155;">{_line_list_html(items, floor=180, ceiling=420, margin_bottom=3)}</ul>'
|
||||
f'</div>'
|
||||
)
|
||||
return (
|
||||
f'<div style="background:{bg}; border:1px solid {border}; border-radius:12px; padding:12px; box-sizing:border-box; height:100%;">'
|
||||
f'<div style="font-size:12px; font-weight:900; color:#0f172a; margin-bottom:8px;">{title}</div>'
|
||||
f'{group_html}'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
|
||||
def _line_list_html(items: list[str], font_px: float = 10.0, line_height: float = 1.55, floor: int = 140, ceiling: int = 420, margin_bottom: int = 4) -> str:
|
||||
return ''.join(
|
||||
f'<li style="margin-bottom:{margin_bottom}px;">{_trim_visible_copy(item, floor=floor, ceiling=ceiling)}</li>'
|
||||
for item in items if item
|
||||
)
|
||||
|
||||
|
||||
def _flatten_group_items(groups: list[dict[str, list[str] | str]]) -> list[str]:
|
||||
flattened: list[str] = []
|
||||
for group in groups:
|
||||
@@ -754,8 +833,9 @@ def _build_goal_image_stakeholder_layout(ctx: PipelineContext, raw: str) -> dict
|
||||
|
||||
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)
|
||||
process_block_raw = _extract_heading_block(raw, process_title)
|
||||
process_lines = _bullet_lines_from_block(process_block_raw, limit=8)
|
||||
process_popup_lines = process_lines[:] or _flatten_group_items(_extract_grouped_bullets(process_block_raw, base_indent=0))
|
||||
|
||||
dx_cards = _load_dx_effect_cards()
|
||||
stakeholder_popup_lines = [f'{title}: {line}' for title, lines in dx_cards for line in lines]
|
||||
@@ -772,85 +852,64 @@ def _build_goal_image_stakeholder_layout(ctx: PipelineContext, raw: str) -> dict
|
||||
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>'
|
||||
'<ul style="margin:0; padding-left:16px; font-size:8.8px; line-height:1.28; 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]
|
||||
),
|
||||
items=_line_list_html([_plain_text(str(item)) for item in group.get('items', [])[:1]], floor=170, ceiling=360, margin_bottom=4),
|
||||
)
|
||||
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_popup = _popup_overlay('popup-goal', goal_title, _popup_list_html(goal_popup_lines, floor=240, ceiling=900)) if goal_popup_lines else ''
|
||||
process_popup = _popup_overlay('popup-process', process_title, _popup_list_html(process_popup_lines, floor=240, ceiling=900)) if process_popup_lines else ''
|
||||
stakeholder_popup = _popup_overlay('popup-stakeholder', support_title, _popup_list_html(stakeholder_popup_lines, floor=240, ceiling=900)) 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.1fr 0.9fr; 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 style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:12px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:15px; font-weight:900; color:#0f172a; margin-bottom:8px;">{goal_title}</div>'
|
||||
'<div style="display:grid; grid-template-columns:1.06fr 0.94fr; gap:10px; align-items:stretch;">'
|
||||
f'<div style="display:flex; flex-direction:column; gap:8px;">{goal_sections_html}</div>'
|
||||
'<div style="display:flex; flex-direction:column; gap:4px;">'
|
||||
f'{_relation_visual(image_src, image_caption).replace("height:220px", "height:132px").replace("padding:10px", "padding:6px")}'
|
||||
f'<div style="font-size:8.5px; line-height:1.2; 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>'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:6px;">{_popup_button("popup-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;">'
|
||||
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 style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:10px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:12.5px; font-weight:900; color:#0f172a; margin-bottom:6px;">{process_title}</div>'
|
||||
f'<ul style="margin:0; padding-left:18px; font-size:9px; line-height:1.34; color:#334155;">{_line_list_html(process_lines[:4], floor=190, ceiling=480, margin_bottom=4)}</ul>'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:6px;">{_popup_button("popup-process", "Details")}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
if dx_cards:
|
||||
stakeholder_cards_html = ''.join(
|
||||
'<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.6px; line-height:1.46; color:#334155;">{"".join(f"<li style=\"margin-bottom:4px;\">{_trim_visible_copy(line, floor=120, ceiling=240)}</li>" for line in lines[:2])}</ul>'
|
||||
'</div></div>'
|
||||
for idx, (title, lines) in enumerate(dx_cards[:3], start=1)
|
||||
stakeholder_body = ''.join(
|
||||
'<div style="margin-bottom:8px;">'
|
||||
f'<div style="font-size:10.5px; font-weight:800; color:#1e3a8a; margin-bottom:3px;">{title}</div>'
|
||||
f'<ul style="margin:0; padding-left:14px; font-size:8.6px; line-height:1.3; color:#334155;">{_line_list_html(lines[:1], floor=170, ceiling=380, margin_bottom=3)}</ul>'
|
||||
'</div>'
|
||||
for title, lines in dx_cards[:3]
|
||||
)
|
||||
else:
|
||||
stakeholder_cards_html = _component_placeholder(support_title, _prefer_source_text(support_topic, 'No stakeholder detail available.'))
|
||||
stakeholder_body = f'<div style="font-size:9.4px; line-height:1.45; color:#475569;">{_trim_visible_copy(_prefer_source_text(support_topic, "No stakeholder detail available."), floor=260, ceiling=560)}</div>'
|
||||
|
||||
stakeholder_card = (
|
||||
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box; display:flex; flex-direction:column;">'
|
||||
f'<div style="font-size:14px; font-weight:900; color:#0f172a; margin-bottom:10px;">{support_title}</div>'
|
||||
f'<div style="display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:10px;">{stakeholder_cards_html}</div>'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:10px;">{_popup_button("popup-stakeholder", "Stakeholder details")}</div>'
|
||||
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:10px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:13px; font-weight:900; color:#0f172a; margin-bottom:8px;">{support_title}</div>'
|
||||
f'{stakeholder_body}'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:6px;">{_popup_button("popup-stakeholder", "??Details")}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
body_inner = (
|
||||
f'{goal_card}'
|
||||
f'{process_card}'
|
||||
f'{stakeholder_card}'
|
||||
f'{goal_popup}{process_popup}{stakeholder_popup}'
|
||||
)
|
||||
lower_block = '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; align-items:start;">' + process_card + stakeholder_card + '</div>'
|
||||
|
||||
body_inner = f'{goal_card}{lower_block}{goal_popup}{process_popup}{stakeholder_popup}'
|
||||
body_html = _type_b_body_shell(body_inner)
|
||||
sidebar_html = '<div style="width:100%; height:100%; opacity:0; pointer-events:none;"></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>'
|
||||
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:5px 16px; text-align:center; color:#ffffff; width:100%; height:40px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:11.5px; font-weight:900; line-height:1.28;">{_trim_visible_copy(conclusion_text, floor=150, ceiling=420)}</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'}
|
||||
|
||||
|
||||
@@ -868,84 +927,84 @@ def _build_requirements_process_product_layout(ctx: PipelineContext, raw: str) -
|
||||
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]
|
||||
process_table_lines = _section_table_lines(raw, process_title, limit=4)
|
||||
|
||||
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_popup = _popup_overlay('popup-req', req_title, _popup_list_html(_flatten_group_items(req_groups), floor=240, ceiling=940))
|
||||
process_popup_lines = _flatten_group_items(process_groups) + process_table_lines
|
||||
process_popup = _popup_overlay('popup-process', process_title, _popup_list_html(process_popup_lines, floor=240, ceiling=940))
|
||||
product_popup = _popup_overlay('popup-product', product_title, _popup_list_html(_flatten_group_items(product_groups), floor=240, ceiling=940))
|
||||
|
||||
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.8px; line-height:1.5; color:#334155;">{items}</ul>'
|
||||
'<div style="background:#ffffff; border:1px solid #d7e2f0; border-top:5px solid {color}; border-radius:12px; padding:10px 12px; min-height:104px;">'
|
||||
'<div style="font-size:11px; font-weight:900; color:#0f172a; margin-bottom:6px;">{title}</div>'
|
||||
'<ul style="margin:0; padding-left:16px; font-size:9.2px; line-height:1.38; 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]
|
||||
),
|
||||
items=_line_list_html([_plain_text(str(item)) for item in group.get('items', [])], floor=190, ceiling=460, margin_bottom=3),
|
||||
)
|
||||
for group, color in zip(req_groups, ['#2563eb', '#7c3aed', '#16a34a'])
|
||||
)
|
||||
requirements_block = (
|
||||
'<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 style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:12px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:14px; font-weight:900; color:#0f172a; margin-bottom:8px;">{req_title}</div>'
|
||||
f'<div style="display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:8px;">{req_cards}</div>'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:6px;">{_popup_button("popup-req", "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.8px; line-height:1.5; color:#334155;">{items}</ul>'
|
||||
process_left_groups = process_groups[:1]
|
||||
process_right_groups = process_groups[1:]
|
||||
process_left_title = process_left_groups[0]['title'] if process_left_groups else process_title
|
||||
process_left_lines = process_table_lines or _flatten_group_items(process_left_groups)
|
||||
process_left_card = (
|
||||
'<div style="background:#f8fafc; border:1px solid #d7e2f0; border-radius:12px; padding:12px; min-height:142px;">'
|
||||
f'<div style="font-size:11px; font-weight:800; color:#1e3a8a; margin-bottom:6px;">{process_left_title}</div>'
|
||||
f'<ul style="margin:0; padding-left:16px; font-size:9.2px; line-height:1.38; color:#334155;">{_line_list_html(process_left_lines[:4], floor=200, ceiling=500, margin_bottom=3)}</ul>'
|
||||
'</div>'
|
||||
)
|
||||
process_right_cards = ''.join(
|
||||
'<div style="background:#f8fafc; border:1px solid #d7e2f0; border-radius:12px; padding:10px 12px; min-height:66px;">'
|
||||
'<div style="font-size:10.8px; font-weight:800; color:#1e3a8a; margin-bottom:5px;">{title}</div>'
|
||||
'<ul style="margin:0; padding-left:16px; font-size:9px; line-height:1.36; 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]
|
||||
),
|
||||
items=_line_list_html([_plain_text(str(item)) for item in group.get('items', [])], floor=190, ceiling=420, margin_bottom=3),
|
||||
)
|
||||
for group in process_groups
|
||||
for group in process_right_groups
|
||||
)
|
||||
process_block = (
|
||||
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box;">'
|
||||
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>'
|
||||
process_card = (
|
||||
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:12px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:13px; font-weight:900; color:#0f172a; margin-bottom:8px;">{process_title}</div>'
|
||||
'<div style="display:grid; grid-template-columns:1.12fr 0.88fr; gap:8px; align-items:start;">'
|
||||
f'{process_left_card}'
|
||||
f'<div style="display:grid; grid-auto-rows:minmax(0,1fr); gap:8px;">{process_right_cards}</div>'
|
||||
'</div>'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:6px;">{_popup_button("popup-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.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=120, ceiling=260)}</li>'
|
||||
for item in group.get('items', [])[:2]
|
||||
),
|
||||
)
|
||||
product_body = ''.join(
|
||||
'<div style="margin-bottom:8px;">'
|
||||
f'<div style="font-size:10.8px; font-weight:800; color:#0f172a; margin-bottom:5px;">{group["title"]}</div>'
|
||||
f'<ul style="margin:0; padding-left:16px; font-size:9.1px; line-height:1.38; color:#334155;">{_line_list_html([_plain_text(str(item)) for item in group.get("items", [])], floor=190, ceiling=480, margin_bottom=3)}</ul>'
|
||||
'</div>'
|
||||
for group in product_groups
|
||||
)
|
||||
product_block = (
|
||||
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:14px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:14px; font-weight:900; color:#0f172a; margin-bottom:10px;">{product_title}</div>'
|
||||
f'<div style="display:grid; grid-template-columns:repeat(3, minmax(0,1fr)); gap:10px;">{product_cards}</div>'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:10px;">{_popup_button("popup-product", "Product details")}</div>'
|
||||
product_card = (
|
||||
'<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:14px; padding:12px; box-sizing:border-box;">'
|
||||
f'<div style="font-size:13px; font-weight:900; color:#0f172a; margin-bottom:8px;">{product_title}</div>'
|
||||
f'{product_body}'
|
||||
f'<div style="display:flex; justify-content:flex-end; margin-top:6px;">{_popup_button("popup-product", "Details")}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
body_inner = (
|
||||
f'{requirements_block}'
|
||||
f'{process_block}'
|
||||
f'{product_block}'
|
||||
f'{req_popup}{process_popup}{product_popup}'
|
||||
)
|
||||
lower_block = '<div style="display:grid; grid-template-columns:1.02fr 0.98fr; gap:10px; align-items:start;">' + process_card + product_card + '</div>'
|
||||
|
||||
body_inner = f'{requirements_block}{lower_block}{req_popup}{process_popup}{product_popup}'
|
||||
body_html = _type_b_body_shell(body_inner)
|
||||
sidebar_html = '<div style="width:100%; height:100%; opacity:0; pointer-events:none;"></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>'
|
||||
footer_html = '<div style="background:linear-gradient(135deg, #0b6ef3 0%, #17a6f5 100%); border-radius:10px; padding:7px 16px; text-align:center; color:#ffffff; width:100%; height:48px; display:flex; align-items:center; justify-content:center; box-sizing:border-box;">' + f'<div style="font-size:11.5px; font-weight:900; line-height:1.28;">{_trim_visible_copy(conclusion_text, floor=150, ceiling=420)}</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'}
|
||||
|
||||
|
||||
@@ -1023,9 +1082,9 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
|
||||
for axis, dx, bim in picked_rows:
|
||||
comparison_rows_html += (
|
||||
'<div style="display:grid; grid-template-columns:1fr 86px 1fr; border-top:1px solid #dbe5f2; align-items:stretch;">'
|
||||
f'<div style="padding:7px 10px; font-size:9.8px; line-height:1.42; color:#1e3a8a; font-weight:600; background:#ffffff;">{_trim_visible_copy(dx, floor=110, ceiling=220)}</div>'
|
||||
f'<div style="padding:7px 10px; font-size:8.8px; line-height:1.28; color:#1e3a8a; font-weight:600; background:#ffffff;">{_trim_visible_copy(dx, floor=110, ceiling=220)}</div>'
|
||||
f'<div style="padding:7px 6px; font-size:9.6px; line-height:1.25; color:#1d4ed8; font-weight:800; text-align:center; background:#eff6ff; border-left:1px solid #dbe5f2; border-right:1px solid #dbe5f2; display:flex; align-items:center; justify-content:center;">{axis}</div>'
|
||||
f'<div style="padding:7px 10px; font-size:9.8px; line-height:1.42; color:#475569; text-align:right; background:#ffffff;">{_trim_visible_copy(bim, floor=110, ceiling=220)}</div>'
|
||||
f'<div style="padding:7px 10px; font-size:8.8px; line-height:1.28; color:#475569; text-align:right; background:#ffffff;">{_trim_visible_copy(bim, floor=110, ceiling=220)}</div>'
|
||||
'</div>'
|
||||
)
|
||||
|
||||
@@ -1158,7 +1217,7 @@ def _build_stage2_retry_html(ctx: PipelineContext, retry_plan: dict) -> dict:
|
||||
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', '?? ?? ?? ??'))
|
||||
support_sidebar = _insert_button_into_card(support_sidebar, _popup_button('popup-support', 'Details Details'))
|
||||
sidebar_parts.append(support_sidebar)
|
||||
|
||||
if not sidebar_parts:
|
||||
|
||||
Reference in New Issue
Block a user