Spaces:
Running on Zero
Running on Zero
zhiyi commited on
Commit ·
eef685d
1
Parent(s): 73fc26e
Align dynamic comparison tables
Browse files- app.py +40 -13
- static/dynamic-content.css +14 -0
- tests/test_app.py +17 -1
app.py
CHANGED
|
@@ -715,6 +715,42 @@ def render_value(value: Any, *, depth: int = 0) -> str:
|
|
| 715 |
return f"<p>{esc(value)}</p>"
|
| 716 |
|
| 717 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
def render_section_content(section: dict[str, Any]) -> str:
|
| 719 |
content, layout = section.get("content", {}), section.get("layout", "text")
|
| 720 |
if layout == "timeline":
|
|
@@ -725,22 +761,13 @@ def render_section_content(section: dict[str, Any]) -> str:
|
|
| 725 |
for n in content.get("nodes", [])
|
| 726 |
) + "</div>"
|
| 727 |
if layout == "comparison":
|
| 728 |
-
columns, rows =
|
| 729 |
head = "".join(f"<th>{esc(x)}</th>" for x in columns)
|
| 730 |
-
def cells(row: dict[str, Any]) -> list[Any]:
|
| 731 |
-
values = row.get("values")
|
| 732 |
-
if isinstance(values, dict):
|
| 733 |
-
return [values.get(column, "") for column in columns]
|
| 734 |
-
if isinstance(values, list):
|
| 735 |
-
return values
|
| 736 |
-
reserved = {"name", "method", "title"}
|
| 737 |
-
return [row.get(column, "") for column in columns] or [v for k, v in row.items() if k not in reserved]
|
| 738 |
body = "".join(
|
| 739 |
-
f'<tr><th>{esc(
|
| 740 |
-
|
| 741 |
-
for row in rows
|
| 742 |
)
|
| 743 |
-
return f'<div class="table-wrap"><table><thead><tr><th>
|
| 744 |
if layout == "process":
|
| 745 |
return '<div class="process">' + "".join(
|
| 746 |
f'<article class="process-step"><span>{i}</span>{render_value(step)}</article>'
|
|
|
|
| 715 |
return f"<p>{esc(value)}</p>"
|
| 716 |
|
| 717 |
|
| 718 |
+
def normalize_comparison(content: dict[str, Any]) -> tuple[str, list[str], list[tuple[str, list[Any]]]]:
|
| 719 |
+
columns = [str(column) for column in content.get("columns", [])]
|
| 720 |
+
raw_rows = content.get("rows", [])
|
| 721 |
+
row_keys = ("name", "dimension", "criterion", "metric", "title", "method")
|
| 722 |
+
row_label = "对比维度"
|
| 723 |
+
|
| 724 |
+
max_values = max(
|
| 725 |
+
(len(row.get("values", [])) for row in raw_rows if isinstance(row, dict) and isinstance(row.get("values"), list)),
|
| 726 |
+
default=0,
|
| 727 |
+
)
|
| 728 |
+
first_is_label = bool(columns) and (
|
| 729 |
+
any(word in columns[0].lower() for word in ("维度", "指标", "dimension", "criterion", "metric"))
|
| 730 |
+
or (max_values and len(columns) == max_values + 1)
|
| 731 |
+
)
|
| 732 |
+
if first_is_label:
|
| 733 |
+
row_label, columns = columns[0], columns[1:]
|
| 734 |
+
|
| 735 |
+
normalized_rows: list[tuple[str, list[Any]]] = []
|
| 736 |
+
for row in raw_rows:
|
| 737 |
+
if not isinstance(row, dict):
|
| 738 |
+
continue
|
| 739 |
+
label = next((str(row[key]) for key in row_keys if row.get(key) not in (None, "")), "")
|
| 740 |
+
values = row.get("values")
|
| 741 |
+
if isinstance(values, dict):
|
| 742 |
+
cells = [values.get(column, "") for column in columns]
|
| 743 |
+
elif isinstance(values, list):
|
| 744 |
+
cells = list(values)
|
| 745 |
+
if len(cells) == len(columns) + 1 and str(cells[0]) == label:
|
| 746 |
+
cells = cells[1:]
|
| 747 |
+
else:
|
| 748 |
+
cells = [row.get(column, "") for column in columns]
|
| 749 |
+
cells = (cells + [""] * len(columns))[:len(columns)]
|
| 750 |
+
normalized_rows.append((label, cells))
|
| 751 |
+
return row_label, columns, normalized_rows
|
| 752 |
+
|
| 753 |
+
|
| 754 |
def render_section_content(section: dict[str, Any]) -> str:
|
| 755 |
content, layout = section.get("content", {}), section.get("layout", "text")
|
| 756 |
if layout == "timeline":
|
|
|
|
| 761 |
for n in content.get("nodes", [])
|
| 762 |
) + "</div>"
|
| 763 |
if layout == "comparison":
|
| 764 |
+
row_label, columns, rows = normalize_comparison(content)
|
| 765 |
head = "".join(f"<th>{esc(x)}</th>" for x in columns)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
body = "".join(
|
| 767 |
+
f'<tr><th scope="row">{esc(label)}</th>{"".join(f"<td>{render_value(value)}</td>" for value in values)}</tr>'
|
| 768 |
+
for label, values in rows
|
|
|
|
| 769 |
)
|
| 770 |
+
return f'<div class="table-wrap"><table class="comparison-table"><thead><tr><th>{esc(row_label)}</th>{head}</tr></thead><tbody>{body}</tbody></table></div>'
|
| 771 |
if layout == "process":
|
| 772 |
return '<div class="process">' + "".join(
|
| 773 |
f'<article class="process-step"><span>{i}</span>{render_value(step)}</article>'
|
static/dynamic-content.css
CHANGED
|
@@ -59,3 +59,17 @@
|
|
| 59 |
border-radius: 8px;
|
| 60 |
background: #231c12;
|
| 61 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
border-radius: 8px;
|
| 60 |
background: #231c12;
|
| 61 |
}
|
| 62 |
+
.comparison-table {
|
| 63 |
+
min-width: 760px;
|
| 64 |
+
table-layout: fixed;
|
| 65 |
+
}
|
| 66 |
+
.comparison-table th:first-child {
|
| 67 |
+
width: 18%;
|
| 68 |
+
}
|
| 69 |
+
.comparison-table td,
|
| 70 |
+
.comparison-table th {
|
| 71 |
+
overflow-wrap: anywhere;
|
| 72 |
+
}
|
| 73 |
+
.comparison-table td > p {
|
| 74 |
+
margin: 0;
|
| 75 |
+
}
|
tests/test_app.py
CHANGED
|
@@ -2,7 +2,7 @@ import unittest
|
|
| 2 |
|
| 3 |
from pydantic import ValidationError
|
| 4 |
|
| 5 |
-
from app import ResearchReport, deduplicate, prepare_evidence_inputs, render_presentation, render_section_content, render_value, safe_slug
|
| 6 |
|
| 7 |
|
| 8 |
def base_report(section):
|
|
@@ -79,6 +79,22 @@ class AppTests(unittest.TestCase):
|
|
| 79 |
self.assertIn("Memory", output)
|
| 80 |
self.assertNotIn("{'", output)
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
if __name__ == "__main__":
|
| 84 |
unittest.main()
|
|
|
|
| 2 |
|
| 3 |
from pydantic import ValidationError
|
| 4 |
|
| 5 |
+
from app import ResearchReport, deduplicate, normalize_comparison, prepare_evidence_inputs, render_presentation, render_section_content, render_value, safe_slug
|
| 6 |
|
| 7 |
|
| 8 |
def base_report(section):
|
|
|
|
| 79 |
self.assertIn("Memory", output)
|
| 80 |
self.assertNotIn("{'", output)
|
| 81 |
|
| 82 |
+
def test_comparison_does_not_duplicate_dimension_column(self):
|
| 83 |
+
content = {
|
| 84 |
+
"columns": ["方法维度", "Debevec & Malik (1997) - HDR重建", "Mertens et al. (2007) - 多曝光融合"],
|
| 85 |
+
"rows": [
|
| 86 |
+
{"name": "核心目标", "values": ["恢复场景物理辐射度图", "直接生成视觉质量高的显示图像"]},
|
| 87 |
+
{"dimension": "是否需要CRF标定", "values": ["是", "否"]},
|
| 88 |
+
],
|
| 89 |
+
}
|
| 90 |
+
row_label, columns, rows = normalize_comparison(content)
|
| 91 |
+
self.assertEqual(row_label, "方法维度")
|
| 92 |
+
self.assertEqual(len(columns), 2)
|
| 93 |
+
self.assertTrue(all(len(values) == 2 for _, values in rows))
|
| 94 |
+
output = render_section_content({"layout": "comparison", "content": content})
|
| 95 |
+
self.assertEqual(output.count("<th>方法维度</th>"), 1)
|
| 96 |
+
self.assertIn("恢复场景物理辐射度图", output)
|
| 97 |
+
|
| 98 |
|
| 99 |
if __name__ == "__main__":
|
| 100 |
unittest.main()
|