hyx/ui/rendering.py

148 lines
4.5 KiB
Python
Raw Normal View History

"""用于把模型生成的 Markdown 摘要做轻量解析并用更产品化的方式展示。
说明不引入复杂依赖不要求模型输出 JSON
通过解析固定标题
- # 一句话结论
- # 关键决策要点Top 5
- # 主要风险Top 3
- # 下一步行动清单
- # 需要进一步澄清的问题
另外提供
- render_opinion_preview观点一句话预览
- summarize_round_opinions把一轮的多角色观点压缩为可扫读摘要用于时间线
"""
from __future__ import annotations
import re
import streamlit as st
SECTION_TITLES = [
"一句话结论",
"关键决策要点Top 5",
"主要风险Top 3",
"下一步行动清单",
"需要进一步澄清的问题",
]
def _split_sections(md: str) -> dict[str, str]:
if not md:
return {}
pattern = r"^#\s+(.*)$"
lines = md.splitlines()
sections: dict[str, list[str]] = {}
current = None
for line in lines:
m = re.match(pattern, line.strip())
if m:
title = m.group(1).strip()
current = title
sections.setdefault(current, [])
continue
if current is not None:
sections[current].append(line)
return {k: "\n".join(v).strip() for k, v in sections.items()}
def _extract_decision_badge(one_liner: str) -> tuple[str, str]:
t = (one_liner or "").strip()
if any(k in t for k in ["不建议", "否决", "不推荐"]):
return ("不建议", "error")
if any(k in t for k in ["谨慎", "有条件", "需要修改"]):
return ("谨慎推进", "warning")
if any(k in t for k in ["推荐", "可行", "建议推进"]):
return ("推荐", "success")
return ("结论", "info")
def render_decision_summary(md: str) -> None:
"""以更产品化的方式渲染结论摘要。"""
sections = _split_sections(md)
one_liner = sections.get("一句话结论", "").strip()
badge_text, badge_kind = _extract_decision_badge(one_liner)
col1, col2 = st.columns([1, 5])
with col1:
if badge_kind == "success":
st.success(badge_text)
elif badge_kind == "warning":
st.warning(badge_text)
elif badge_kind == "error":
st.error(badge_text)
else:
st.info(badge_text)
with col2:
if one_liner:
st.markdown(f"**{one_liner}**")
else:
st.markdown("**(未识别到一句话结论,请检查模型输出格式)**")
st.markdown("---")
c1, c2 = st.columns(2)
with c1:
st.subheader("🎯 关键决策要点")
content = sections.get("关键决策要点Top 5", "").strip()
st.markdown(content if content else "(暂无)")
st.subheader("✅ 下一步行动")
actions = sections.get("下一步行动清单", "").strip()
items = [
re.sub(r"^-\s*\[.\]\s*", "", l).strip()
for l in actions.splitlines()
if l.strip().startswith("-")
]
if items:
for i, it in enumerate(items):
st.checkbox(it, value=False, key=f"action_{i}")
else:
st.markdown(actions if actions else "(暂无)")
with c2:
st.subheader("⚠️ 主要风险")
risks = sections.get("主要风险Top 3", "").strip()
st.markdown(risks if risks else "(暂无)")
st.subheader("❓ 需要澄清")
qs = sections.get("需要进一步澄清的问题", "").strip()
st.markdown(qs if qs else "(暂无)")
def render_opinion_preview(opinion: str, limit: int = 160) -> str:
"""返回用于预览的短文本。"""
if not opinion:
return ""
t = opinion.strip().replace("\n", " ")
if len(t) <= limit:
return t
return t[:limit].rstrip() + "..."
def summarize_round_opinions(opinions: dict, role_descriptions: dict, limit_per_role: int = 60) -> str:
"""把一轮 opinions 生成适合扫读的摘要(不调用模型,避免额外成本与不稳定)。
输出格式示例
- 📊 产品经理xxx...
- 💻 技术专家xxx...
"""
if not opinions:
return ""
lines: list[str] = []
for role, text in opinions.items():
info = role_descriptions.get(role, {"icon": "👤", "name": role})
preview = render_opinion_preview(str(text or ""), limit=limit_per_role)
if not preview:
preview = "(本轮未生成/生成失败)"
lines.append(f"- {info['icon']} {info['name']}{preview}")
return "\n".join(lines)