马一丁

Optimize the Rendering Process

@@ -1028,6 +1028,75 @@ class HTMLRenderer: @@ -1028,6 +1028,75 @@ class HTMLRenderer:
1028 """ 1028 """
1029 return f'<div class="kpi-grid">{cards}</div>' 1029 return f'<div class="kpi-grid">{cards}</div>'
1030 1030
  1031 + def _merge_dicts(
  1032 + self, base: Dict[str, Any] | None, override: Dict[str, Any] | None
  1033 + ) -> Dict[str, Any]:
  1034 + """
  1035 + 递归合并两个字典,override覆盖base,均为新副本,避免副作用。
  1036 + """
  1037 + result = copy.deepcopy(base) if isinstance(base, dict) else {}
  1038 + if not isinstance(override, dict):
  1039 + return result
  1040 + for key, value in override.items():
  1041 + if isinstance(value, dict) and isinstance(result.get(key), dict):
  1042 + result[key] = self._merge_dicts(result[key], value)
  1043 + else:
  1044 + result[key] = copy.deepcopy(value)
  1045 + return result
  1046 +
  1047 + def _looks_like_chart_dataset(self, candidate: Any) -> bool:
  1048 + """启发式判断对象是否包含Chart.js常见的labels/datasets结构"""
  1049 + if not isinstance(candidate, dict):
  1050 + return False
  1051 + labels = candidate.get("labels")
  1052 + datasets = candidate.get("datasets")
  1053 + return isinstance(labels, list) or isinstance(datasets, list)
  1054 +
  1055 + def _coerce_chart_data_structure(self, data: Dict[str, Any]) -> Dict[str, Any]:
  1056 + """
  1057 + 兼容LLM输出的Chart.js完整配置(含type/data/options)。
  1058 + 若data中嵌套一个真正的labels/datasets结构,则提取并返回该结构。
  1059 + """
  1060 + if not isinstance(data, dict):
  1061 + return {}
  1062 + if self._looks_like_chart_dataset(data):
  1063 + return data
  1064 + for key in ("data", "chartData", "payload"):
  1065 + nested = data.get(key)
  1066 + if self._looks_like_chart_dataset(nested):
  1067 + return copy.deepcopy(nested)
  1068 + return data
  1069 +
  1070 + def _prepare_widget_payload(
  1071 + self, block: Dict[str, Any]
  1072 + ) -> tuple[Dict[str, Any], Dict[str, Any]]:
  1073 + """
  1074 + 预处理widget数据,兼容部分block将Chart.js配置写入data字段的情况。
  1075 +
  1076 + 返回:
  1077 + tuple(props, data): 归一化后的props与chart数据
  1078 + """
  1079 + props = copy.deepcopy(block.get("props") or {})
  1080 + raw_data = block.get("data")
  1081 + data_copy = copy.deepcopy(raw_data) if isinstance(raw_data, dict) else raw_data
  1082 + widget_type = block.get("widgetType") or ""
  1083 + chart_like = isinstance(widget_type, str) and widget_type.startswith("chart.js")
  1084 +
  1085 + if chart_like and isinstance(data_copy, dict):
  1086 + inline_options = data_copy.pop("options", None)
  1087 + inline_type = data_copy.pop("type", None)
  1088 + normalized_data = self._coerce_chart_data_structure(data_copy)
  1089 + if isinstance(inline_options, dict):
  1090 + props["options"] = self._merge_dicts(props.get("options"), inline_options)
  1091 + if isinstance(inline_type, str) and inline_type and not props.get("type"):
  1092 + props["type"] = inline_type
  1093 + elif isinstance(data_copy, dict):
  1094 + normalized_data = data_copy
  1095 + else:
  1096 + normalized_data = {}
  1097 +
  1098 + return props, normalized_data
  1099 +
1031 def _render_widget(self, block: Dict[str, Any]) -> str: 1100 def _render_widget(self, block: Dict[str, Any]) -> str:
1032 """ 1101 """
1033 渲染Chart.js等交互组件的占位容器,并记录配置JSON。 1102 渲染Chart.js等交互组件的占位容器,并记录配置JSON。
@@ -1042,11 +1111,12 @@ class HTMLRenderer: @@ -1042,11 +1111,12 @@ class HTMLRenderer:
1042 canvas_id = f"chart-{self.chart_counter}" 1111 canvas_id = f"chart-{self.chart_counter}"
1043 config_id = f"chart-config-{self.chart_counter}" 1112 config_id = f"chart-config-{self.chart_counter}"
1044 1113
  1114 + props, normalized_data = self._prepare_widget_payload(block)
1045 payload = { 1115 payload = {
1046 "widgetId": block.get("widgetId"), 1116 "widgetId": block.get("widgetId"),
1047 "widgetType": block.get("widgetType"), 1117 "widgetType": block.get("widgetType"),
1048 - "props": block.get("props", {}),  
1049 - "data": block.get("data", {}), 1118 + "props": props,
  1119 + "data": normalized_data,
1050 "dataRef": block.get("dataRef"), 1120 "dataRef": block.get("dataRef"),
1051 } 1121 }
1052 config_json = json.dumps(payload, ensure_ascii=False).replace("</", "<\\/") 1122 config_json = json.dumps(payload, ensure_ascii=False).replace("</", "<\\/")
@@ -1054,9 +1124,9 @@ class HTMLRenderer: @@ -1054,9 +1124,9 @@ class HTMLRenderer:
1054 f'<script type="application/json" id="{config_id}">{config_json}</script>' 1124 f'<script type="application/json" id="{config_id}">{config_json}</script>'
1055 ) 1125 )
1056 1126
1057 - title = block.get("props", {}).get("title") 1127 + title = props.get("title")
1058 title_html = f'<div class="chart-title">{self._escape_html(title)}</div>' if title else "" 1128 title_html = f'<div class="chart-title">{self._escape_html(title)}</div>' if title else ""
1059 - fallback_html = self._render_widget_fallback(block) 1129 + fallback_html = self._render_widget_fallback(normalized_data)
1060 return f""" 1130 return f"""
1061 <div class="chart-card"> 1131 <div class="chart-card">
1062 {title_html} 1132 {title_html}
@@ -1067,9 +1137,10 @@ class HTMLRenderer: @@ -1067,9 +1137,10 @@ class HTMLRenderer:
1067 </div> 1137 </div>
1068 """ 1138 """
1069 1139
1070 - def _render_widget_fallback(self, block: Dict[str, Any]) -> str: 1140 + def _render_widget_fallback(self, data: Dict[str, Any]) -> str:
1071 """渲染图表数据的文本兜底视图,避免Chart.js加载失败时出现空白""" 1141 """渲染图表数据的文本兜底视图,避免Chart.js加载失败时出现空白"""
1072 - data = block.get("data") or {} 1142 + if not isinstance(data, dict):
  1143 + return ""
1073 labels = data.get("labels") or [] 1144 labels = data.get("labels") or []
1074 datasets = data.get("datasets") or [] 1145 datasets = data.get("datasets") or []
1075 if not labels or not datasets: 1146 if not labels or not datasets: