tab3.py 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. #!/usr/bin/env python3
  2. """
  3. Tab3内容生成器 - 脚本点(元素列表)
  4. """
  5. import html as html_module
  6. from typing import Dict, Any, List
  7. def get_intent_support_data(element: Dict[str, Any]) -> Dict[str, Any]:
  8. """
  9. 获取元素的意图支撑数据(兼容新旧数据结构)
  10. 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  11. 这两个字段内部存储的都是意图支撑数据
  12. Args:
  13. element: 元素数据
  14. Returns:
  15. 意图支撑数据字典,格式:{"灵感点": [...], "目的点": [...], "关键点": [...]}
  16. """
  17. # 优先使用"意图支撑"字段
  18. intent_support = element.get('意图支撑')
  19. if intent_support and isinstance(intent_support, dict):
  20. return intent_support
  21. # 如果没有"意图支撑",则使用"多维度评分"字段(兼容旧数据)
  22. multi_scores = element.get('多维度评分')
  23. if multi_scores and isinstance(multi_scores, dict):
  24. return multi_scores
  25. # 都没有则返回空字典
  26. return {}
  27. def calculate_intent_support_count(element: Dict[str, Any]) -> int:
  28. """
  29. 计算元素的意图支撑数量
  30. 统计元素支撑的意图点总数
  31. 支撑的意图点越多,说明该元素与意图的关联越强
  32. Args:
  33. element: 元素数据
  34. Returns:
  35. 支撑的意图点总数
  36. """
  37. # 统一使用意图支撑数据统计(实质和形式都使用相同逻辑)
  38. # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  39. intent_support_data = get_intent_support_data(element)
  40. total_support_count = 0
  41. for point_type in ['灵感点', '目的点', '关键点']:
  42. support_points = intent_support_data.get(point_type, []) or []
  43. # 每个项目就是一个支撑点,直接统计数量
  44. total_support_count += len(support_points)
  45. return total_support_count
  46. def get_support_stats(element: Dict[str, Any]) -> Dict[str, int]:
  47. """
  48. 获取元素的支撑统计信息(灵感点/目的点/关键点数量)
  49. 优先使用元素中已经预计算好的 support_stats 字段;
  50. 若不存在,则根据「意图支撑」或「多维度评分」字段动态统计。
  51. """
  52. # 优先使用预计算的支撑统计
  53. support_stats = element.get('支撑统计')
  54. if isinstance(support_stats, dict):
  55. # 做一次安全拷贝并补齐缺失字段
  56. return {
  57. '灵感点数量': int(support_stats.get('灵感点数量', 0) or 0),
  58. '目的点数量': int(support_stats.get('目的点数量', 0) or 0),
  59. '关键点数量': int(support_stats.get('关键点数量', 0) or 0),
  60. }
  61. # 兼容旧字段名
  62. support_stats_old = element.get('support_stats')
  63. if isinstance(support_stats_old, dict):
  64. return {
  65. '灵感点数量': int(support_stats_old.get('灵感点数量', 0) or 0),
  66. '目的点数量': int(support_stats_old.get('目的点数量', 0) or 0),
  67. '关键点数量': int(support_stats_old.get('关键点数量', 0) or 0),
  68. }
  69. # 统一使用意图支撑数据统计(实质和形式都使用相同逻辑)
  70. # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  71. intent_support_data = get_intent_support_data(element)
  72. return {
  73. '灵感点数量': len(intent_support_data.get('灵感点', []) or []),
  74. '目的点数量': len(intent_support_data.get('目的点', []) or []),
  75. '关键点数量': len(intent_support_data.get('关键点', []) or []),
  76. }
  77. def compute_weight_scores(element: Dict[str, Any]) -> Dict[str, Any]:
  78. """
  79. 计算元素的权重相关得分。
  80. 对于形式元素:优先使用元素中预计算的权重分和权重明细
  81. 对于实质元素:根据共性分析和意图支撑动态计算
  82. 原始总分 = 各子项得分之和
  83. 权重分 = min(100, 原始总分 × 100 / 110)
  84. """
  85. # 判断是形式元素还是实质元素
  86. dimension = element.get('维度') or {}
  87. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  88. # 形式元素:优先使用预计算的权重信息
  89. if is_form:
  90. weight_score = element.get('权重分')
  91. weight_details = element.get('权重明细')
  92. support_stats = get_support_stats(element)
  93. # 如果存在预计算的权重信息,直接使用
  94. if weight_score is not None and weight_details is not None:
  95. # 从权重明细中提取各项得分
  96. freq_score = float(weight_details.get('频次分', 0) or 0)
  97. coverage_count_score = float(weight_details.get('覆盖段落数分', 0) or 0)
  98. coverage_rate_score = float(weight_details.get('覆盖率分', 0) or 0)
  99. inspiration_score = float(weight_details.get('灵感点支撑分', 0) or 0)
  100. purpose_score = float(weight_details.get('目的点支撑分', 0) or 0)
  101. keypoint_score = float(weight_details.get('关键点支撑分', 0) or 0)
  102. # 计算原始总分(共性总分 + 支撑总分)
  103. commonality_total = weight_details.get('共性总分', 0) or 0
  104. support_total = weight_details.get('支撑总分', 0) or 0
  105. raw_total = float(commonality_total) + float(support_total)
  106. return {
  107. 'weight_score': float(weight_score),
  108. 'raw_total': raw_total,
  109. 'details': {
  110. '频次分': freq_score,
  111. '覆盖段落数分': coverage_count_score,
  112. '覆盖率分': coverage_rate_score,
  113. '灵感点支撑分': inspiration_score,
  114. '目的点支撑分': purpose_score,
  115. '关键点支撑分': keypoint_score,
  116. },
  117. 'support_stats': support_stats,
  118. }
  119. # 实质元素或形式元素没有预计算权重:使用旧的计算逻辑
  120. commonality = element.get('共性分析') or {}
  121. coverage = float(commonality.get('段落覆盖率', 0.0) or 0.0)
  122. frequency = int(commonality.get('出现频次', 0) or 0)
  123. support_stats = get_support_stats(element)
  124. inspiration_count = support_stats.get('灵感点数量', 0) or 0
  125. purpose_count = support_stats.get('目的点数量', 0) or 0
  126. keypoint_count = support_stats.get('关键点数量', 0) or 0
  127. # 1) 频次分(0–30分)
  128. # 假定「高频」的参考上限为 12 次,超过即视为满分
  129. # 频次分 = min(30, 出现频次 / 12 * 30)
  130. if frequency <= 0:
  131. freq_score = 0.0
  132. else:
  133. freq_score = min(30.0, frequency * 30.0 / 12.0)
  134. # 2) 覆盖率分(0–30分)
  135. # 覆盖率分 = 段落覆盖率 × 30
  136. coverage_score = max(0.0, min(30.0, coverage * 30.0))
  137. # 3) 灵感点支撑分(0–25分)
  138. # 按你的说明:支撑{灵感点数量}个灵感点 × 25分/个,封顶 25 分
  139. inspiration_score = min(25.0, float(inspiration_count) * 25.0)
  140. # 4) 目的点支撑分(0–15分)
  141. # 说明:支撑{目的点数量}个目的点 × 5分/个,封顶 15 分
  142. purpose_score = min(15.0, float(purpose_count) * 5.0)
  143. # 5) 关键点支撑分(0–10分)
  144. # 说明:支撑{关键点数量}个关键点 × 1分/个,封顶 10 分
  145. keypoint_score = min(10.0, float(keypoint_count) * 1.0)
  146. raw_total = freq_score + coverage_score + inspiration_score + purpose_score + keypoint_score
  147. if raw_total <= 0:
  148. weight_score = 0.0
  149. else:
  150. weight_score = min(100.0, raw_total * 100.0 / 110.0)
  151. return {
  152. 'weight_score': weight_score,
  153. 'raw_total': raw_total,
  154. 'details': {
  155. '频次分': freq_score,
  156. '覆盖率分': coverage_score,
  157. '灵感点支撑分': inspiration_score,
  158. '目的点支撑分': purpose_score,
  159. '关键点支撑分': keypoint_score,
  160. },
  161. 'support_stats': support_stats,
  162. }
  163. def determine_dominant_factor(element: Dict[str, Any], all_elements: List[Dict[str, Any]]) -> str:
  164. """
  165. 判断元素排序的主导因素
  166. 排序规则:覆盖率 > 频次 > 意图支撑数
  167. 主导因素判断:哪个指标在当前元素中相对最显著
  168. Args:
  169. element: 当前元素
  170. all_elements: 同组所有元素
  171. Returns:
  172. 主导因素: 'coverage' | 'frequency' | 'intent_support'
  173. """
  174. if not all_elements:
  175. return 'coverage'
  176. # 获取当前元素的指标
  177. commonality = element.get('共性分析') or {}
  178. coverage = commonality.get('段落覆盖率', 0.0)
  179. frequency = commonality.get('出现频次', 0)
  180. intent_count = calculate_intent_support_count(element)
  181. # 将所有指标归一化到同一量级,然后比较
  182. # 覆盖率已经是0-1范围
  183. # 频次归一化:假设最大频次为10
  184. normalized_frequency = min(frequency / 10.0, 1.0)
  185. # 意图支撑数归一化:假设最大支撑数为10
  186. normalized_intent = min(intent_count / 10.0, 1.0)
  187. # 比较归一化后的值,取最大的作为主导因素
  188. scores = {
  189. 'coverage': coverage,
  190. 'frequency': normalized_frequency,
  191. 'intent_support': normalized_intent
  192. }
  193. return max(scores, key=scores.get)
  194. def sort_elements_by_coverage_and_frequency(elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
  195. """
  196. 按照覆盖率、频次和意图支撑数对元素排序
  197. 排序规则:
  198. 1. 第一优先级:共性(段落覆盖率)- 倒序
  199. 2. 第二优先级:共性(出现频次)- 倒序
  200. 3. 第三优先级:意图支撑数 - 倒序
  201. Args:
  202. elements: 元素列表
  203. Returns:
  204. 排序后的元素列表
  205. """
  206. def get_sort_key(elem):
  207. # 获取共性分析,防止为None
  208. commonality = elem.get('共性分析') or {}
  209. # 获取段落覆盖率
  210. coverage = commonality.get('段落覆盖率', 0.0)
  211. # 获取出现频次
  212. frequency = commonality.get('出现频次', 0)
  213. # 计算意图支撑数
  214. intent_count = calculate_intent_support_count(elem)
  215. # 返回排序键(负数用于倒序)
  216. return (-coverage, -frequency, -intent_count)
  217. return sorted(elements, key=get_sort_key)
  218. def get_element_category(element: Dict[str, Any]) -> str:
  219. """
  220. 获取元素的分类名称(支持新旧两种数据结构)
  221. Args:
  222. element: 元素数据
  223. Returns:
  224. 分类名称字符串
  225. """
  226. category = element.get('分类', '未分类')
  227. if isinstance(category, dict):
  228. # 新结构:分类是对象,包含一级分类和二级分类
  229. level1 = category.get('一级分类', '')
  230. level2 = category.get('二级分类', '')
  231. if level1 and level2:
  232. return f"{level1} - {level2}"
  233. elif level1:
  234. return level1
  235. else:
  236. return '未分类'
  237. else:
  238. # 旧结构:分类是字符串或列表
  239. if isinstance(category, list):
  240. return ' - '.join(category) if category else '未分类'
  241. return category if category else '未分类'
  242. def group_elements_by_hierarchical_category(elements: List[Dict[str, Any]]) -> Dict[str, Any]:
  243. """
  244. 按树形分类结构组织元素(一级分类 → 二级分类 → 元素)
  245. 优化规则:同一个父节点下的所有子节点采用统一的分类格式展示
  246. - 如果一级分类下既有元素又有分类,将元素归入"未分类"二级分类
  247. Args:
  248. elements: 元素列表
  249. Returns:
  250. 树形分类结构字典
  251. """
  252. # 1. 按一级分类和二级分类分组
  253. level1_groups = {}
  254. for elem in elements:
  255. category_data = elem.get('分类', {})
  256. if isinstance(category_data, dict):
  257. level1 = category_data.get('一级分类', '未分类')
  258. level2 = category_data.get('二级分类', '')
  259. elif isinstance(category_data, list):
  260. # 列表格式:第一个元素作为一级分类,第二个作为二级分类
  261. level1 = category_data[0] if len(category_data) > 0 else '未分类'
  262. level2 = category_data[1] if len(category_data) > 1 else ''
  263. else:
  264. # 旧结构:分类是字符串
  265. level1 = str(category_data) if category_data else '未分类'
  266. level2 = ''
  267. # 初始化一级分类
  268. if level1 not in level1_groups:
  269. level1_groups[level1] = {
  270. 'elements': [],
  271. 'level2_groups': {}
  272. }
  273. # 如果有二级分类,放入二级分类组;否则放入一级分类的直接元素列表(临时)
  274. if level2:
  275. if level2 not in level1_groups[level1]['level2_groups']:
  276. level1_groups[level1]['level2_groups'][level2] = []
  277. level1_groups[level1]['level2_groups'][level2].append(elem)
  278. else:
  279. level1_groups[level1]['elements'].append(elem)
  280. # 1.5 优化:仅当一级分类下既有直接元素又有二级分类时,才将直接元素移到"未分类"二级分类中
  281. # 如果一级分类下只有直接元素,没有二级分类,则保持原样(不需要"未分类"概念)
  282. for level1_name, level1_data in level1_groups.items():
  283. if level1_data['elements'] and level1_data['level2_groups']:
  284. # 将直接元素移到"未分类"分类
  285. if '未分类' not in level1_data['level2_groups']:
  286. level1_data['level2_groups']['未分类'] = []
  287. level1_data['level2_groups']['未分类'].extend(level1_data['elements'])
  288. level1_data['elements'] = []
  289. # 如果只有直接元素,没有二级分类,则保持level1_data['elements']不变
  290. # 2. 对每个分类内的元素排序
  291. for level1_data in level1_groups.values():
  292. # 排序一级分类直接包含的元素
  293. if level1_data['elements']:
  294. level1_data['elements'] = sort_elements_by_coverage_and_frequency(level1_data['elements'])
  295. # 排序每个二级分类的元素
  296. for level2_name in level1_data['level2_groups']:
  297. level1_data['level2_groups'][level2_name] = sort_elements_by_coverage_and_frequency(
  298. level1_data['level2_groups'][level2_name]
  299. )
  300. # 3. 计算每个一级分类的统计信息用于排序
  301. level1_scores = {}
  302. for level1_name, level1_data in level1_groups.items():
  303. # 收集该一级分类下的所有元素(包括二级分类下的)
  304. all_elements = level1_data['elements'][:]
  305. for level2_elements in level1_data['level2_groups'].values():
  306. all_elements.extend(level2_elements)
  307. if not all_elements:
  308. level1_scores[level1_name] = (0.0, 0, 0.0)
  309. continue
  310. # 计算统计指标
  311. avg_coverage = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in all_elements) / len(all_elements)
  312. avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in all_elements) / len(all_elements)
  313. avg_intent_count = sum(calculate_intent_support_count(e) for e in all_elements) / len(all_elements)
  314. level1_scores[level1_name] = (avg_coverage, avg_frequency, avg_intent_count)
  315. # 4. 对一级分类排序
  316. sorted_level1 = sorted(
  317. level1_scores.keys(),
  318. key=lambda c: (-level1_scores[c][0], -level1_scores[c][1], -level1_scores[c][2])
  319. )
  320. # 5. 对每个一级分类内的二级分类排序
  321. for level1_name in sorted_level1:
  322. level1_data = level1_groups[level1_name]
  323. level2_groups = level1_data['level2_groups']
  324. if not level2_groups:
  325. continue
  326. # 计算二级分类的统计信息
  327. level2_scores = {}
  328. for level2_name, level2_elements in level2_groups.items():
  329. if not level2_elements:
  330. level2_scores[level2_name] = (0.0, 0, 0.0)
  331. continue
  332. avg_coverage = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in level2_elements) / len(level2_elements)
  333. avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in level2_elements) / len(level2_elements)
  334. avg_intent_count = sum(calculate_intent_support_count(e) for e in level2_elements) / len(level2_elements)
  335. level2_scores[level2_name] = (avg_coverage, avg_frequency, avg_intent_count)
  336. # 排序二级分类
  337. sorted_level2_names = sorted(
  338. level2_scores.keys(),
  339. key=lambda c: (-level2_scores[c][0], -level2_scores[c][1], -level2_scores[c][2])
  340. )
  341. # 重新组织为有序字典
  342. sorted_level2_groups = {name: level2_groups[name] for name in sorted_level2_names}
  343. level1_data['level2_groups'] = sorted_level2_groups
  344. # 6. 返回排序后的结构
  345. return {level1_name: level1_groups[level1_name] for level1_name in sorted_level1}
  346. def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, Any]] = None) -> str:
  347. """渲染单个元素项的HTML(支持详情展开,兼容新旧数据结构)
  348. Args:
  349. element: 元素数据
  350. all_elements: 同组所有元素(用于计算主导因素)
  351. """
  352. elem_id = element.get('id', '')
  353. name = element.get('名称') or '' # 处理None的情况
  354. description = element.get('描述') or '' # 处理None的情况
  355. # 获取类型和维度(兼容新旧结构)
  356. dimension = element.get('维度', {})
  357. if isinstance(dimension, dict):
  358. elem_type = dimension.get('一级', '')
  359. elem_type_level2 = dimension.get('二级', '')
  360. else:
  361. elem_type = element.get('类型', '')
  362. elem_type_level2 = ''
  363. # 获取分类(兼容新旧结构)
  364. category_data = element.get('分类', '')
  365. if isinstance(category_data, dict):
  366. category_level1 = category_data.get('一级分类', '')
  367. category_level2 = category_data.get('二级分类', '')
  368. category = get_element_category(element)
  369. elif isinstance(category_data, list):
  370. category_level1 = category_data[0] if len(category_data) > 0 else ''
  371. category_level2 = category_data[1] if len(category_data) > 1 else ''
  372. category = get_element_category(element)
  373. else:
  374. category = category_data
  375. category_level1 = ''
  376. category_level2 = ''
  377. category_def = element.get('分类定义', '')
  378. # 获取共性分析(防止为None)
  379. commonality = element.get('共性分析') or {}
  380. coverage = commonality.get('段落覆盖率', 0.0)
  381. frequency = commonality.get('出现频次', 0)
  382. paragraphs_list = commonality.get('出现段落列表', [])
  383. source = element.get('来源', [])
  384. intent_count = calculate_intent_support_count(element)
  385. intent_support = get_intent_support_data(element)
  386. # 计算权重得分(如果有 weight_details)
  387. weight_info = compute_weight_scores(element)
  388. # 检查是否有详细信息
  389. has_details = bool(elem_type or category or category_def or paragraphs_list or source or intent_support or weight_info.get('raw_total', 0) > 0)
  390. # 计算主导因素
  391. dominant_factor = 'coverage' # 默认
  392. if all_elements:
  393. dominant_factor = determine_dominant_factor(element, all_elements)
  394. # 根据主导因素确定边框颜色
  395. border_color_class = f'dominant-{dominant_factor}'
  396. html = f'<li class="element-item {border_color_class}" data-elem-id="{elem_id}">\n'
  397. html += '<div class="element-header" onclick="toggleElementDetails(this)">\n'
  398. # 添加展开/收起图标
  399. if has_details:
  400. html += '<span class="element-toggle-icon">▶</span>\n'
  401. # 显示ID和名称
  402. if elem_id:
  403. html += f'<span class="element-id">#{elem_id}</span>\n'
  404. html += f'<span class="element-name">{html_module.escape(name)}</span>\n'
  405. # 显示统计指标(根据主导因素高亮)
  406. # 判断是否为形式元素
  407. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  408. html += '<div class="element-stats">\n'
  409. # 显示权重分(如有)
  410. if weight_info.get('raw_total', 0) > 0:
  411. html += f'<span class="stat-badge stat-weight">权重分: {weight_info["weight_score"]:.1f}</span>\n'
  412. if is_form:
  413. # 形式元素:显示支撑数量和权重分
  414. html += f'<span class="stat-badge stat-intent">支撑: {intent_count}</span>\n'
  415. # 权重分已在上面显示
  416. else:
  417. # 实质元素显示全部三个指标
  418. coverage_highlight = 'stat-highlight' if dominant_factor == 'coverage' else ''
  419. frequency_highlight = 'stat-highlight' if dominant_factor == 'frequency' else ''
  420. intent_highlight = 'stat-highlight' if dominant_factor == 'intent_support' else ''
  421. html += f'<span class="stat-badge stat-coverage {coverage_highlight}">覆盖率: {coverage:.2%}</span>\n'
  422. html += f'<span class="stat-badge stat-frequency {frequency_highlight}">频次: {frequency}</span>\n'
  423. html += f'<span class="stat-badge stat-intent {intent_highlight}">意图支撑: {intent_count}</span>\n'
  424. html += '</div>\n'
  425. html += '</div>\n'
  426. # 描述(始终显示)
  427. if description:
  428. html += f'<div class="element-description">{html_module.escape(description)}</div>\n'
  429. # 详细信息(可展开)
  430. if has_details:
  431. html += '<div class="element-details collapsed">\n'
  432. # 维度/类型 - 已移除,不再展示
  433. # if elem_type:
  434. # html += '<div class="detail-section">\n'
  435. # html += '<strong>维度:</strong>\n'
  436. # html += '<div class="detail-content">\n'
  437. # html += f'<span class="detail-tag dimension-level1">{html_module.escape(elem_type)}</span>\n'
  438. # if elem_type_level2:
  439. # html += f'<span class="detail-tag dimension-level2">{html_module.escape(elem_type_level2)}</span>\n'
  440. # html += '</div>\n'
  441. # html += '</div>\n'
  442. # 分类 - 已移除,不再展示
  443. # if category_level1 or category:
  444. # html += '<div class="detail-section">\n'
  445. # html += '<strong>分类:</strong>\n'
  446. # html += '<div class="detail-content">\n'
  447. # if category_level1:
  448. # html += f'<span class="detail-tag category-level1">{html_module.escape(category_level1)}</span>\n'
  449. # if category_level2:
  450. # html += f'<span class="detail-tag category-level2">{html_module.escape(category_level2)}</span>\n'
  451. # else:
  452. # html += f'<span class="detail-tag">{html_module.escape(str(category))}</span>\n'
  453. # html += '</div>\n'
  454. # html += '</div>\n'
  455. # 分类定义
  456. if category_def:
  457. html += '<div class="detail-section">\n'
  458. html += '<strong>分类定义:</strong>\n'
  459. html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
  460. html += '</div>\n'
  461. # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
  462. if isinstance(dimension, dict) and dimension.get('一级') == '形式':
  463. # 支撑
  464. zhicheng = element.get('支撑')
  465. if zhicheng:
  466. html += '<div class="detail-section">\n'
  467. html += '<strong>支撑:</strong>\n'
  468. html += '<div class="detail-content">\n'
  469. if isinstance(zhicheng, list):
  470. for item in zhicheng:
  471. if isinstance(item, dict):
  472. item_id = item.get('id', '')
  473. item_name = item.get('名称', '')
  474. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  475. else:
  476. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  477. elif isinstance(zhicheng, dict):
  478. # 支撑可能是对象(包含具体元素、具象概念等)
  479. for key, values in zhicheng.items():
  480. if isinstance(values, list):
  481. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  482. for item in values:
  483. if isinstance(item, dict):
  484. item_id = item.get('id', '')
  485. item_name = item.get('名称', '')
  486. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  487. else:
  488. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  489. else:
  490. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  491. else:
  492. html += f'<span class="detail-tag">{html_module.escape(str(zhicheng))}</span>\n'
  493. html += '</div>\n'
  494. html += '</div>\n'
  495. # 推理
  496. tuili = element.get('推理')
  497. if tuili:
  498. html += '<div class="detail-section">\n'
  499. html += '<strong>推理:</strong>\n'
  500. html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
  501. html += '</div>\n'
  502. # 支撑关系(形式元素专用,从意图支撑数据中显示支撑点)
  503. # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  504. intent_support_data = get_intent_support_data(element)
  505. if intent_support_data:
  506. html += '<div class="detail-section">\n'
  507. html += '<strong>支撑关系:</strong>\n'
  508. for point_type in ['灵感点', '目的点', '关键点']:
  509. support_points = intent_support_data.get(point_type) or []
  510. if not support_points:
  511. continue
  512. html += f'<div class="score-type">{point_type}</div>\n'
  513. html += '<div class="score-list">\n'
  514. # 每个项目就是一个支撑点
  515. for support_point in support_points:
  516. if not isinstance(support_point, dict):
  517. continue
  518. point = support_point.get('点', '')
  519. point_intention = support_point.get('点的意图', '')
  520. support_reason = support_point.get('支撑理由', '')
  521. html += '<div class="score-item">\n'
  522. html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
  523. # 显示点的意图
  524. if point_intention:
  525. html += '<div class="point-intention">\n'
  526. html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
  527. html += '</div>\n'
  528. # 显示支撑理由
  529. if support_reason:
  530. html += '<div class="score-reasons">\n'
  531. html += f'<strong style="color: #666;">支撑理由:</strong>\n'
  532. html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
  533. html += '</div>\n'
  534. html += '</div>\n'
  535. html += '</div>\n' # end score-list
  536. html += '</div>\n' # end detail-section
  537. # 针对"隐含概念",显示"来源"(声音特征、语气语调、BGM、音效等)和"时间范围"
  538. elem_type = element.get('类型', '')
  539. if elem_type == '隐含概念':
  540. # 来源(隐含概念的来源包含声音特征、语气语调、背景音乐、音效等)
  541. laiyuan = element.get('来源')
  542. if laiyuan and isinstance(laiyuan, dict):
  543. html += '<div class="detail-section">\n'
  544. html += '<strong>来源:</strong>\n'
  545. html += '<div class="detail-content">\n'
  546. for key, values in laiyuan.items():
  547. if isinstance(values, list) and values:
  548. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  549. for item in values:
  550. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  551. elif values:
  552. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  553. html += '</div>\n'
  554. html += '</div>\n'
  555. # 时间范围
  556. time_range = element.get('时间范围')
  557. if time_range:
  558. html += '<div class="detail-section">\n'
  559. html += '<strong>时间范围:</strong>\n'
  560. html += '<div class="detail-content">\n'
  561. if isinstance(time_range, list):
  562. for tr in time_range:
  563. html += f'<span class="detail-tag source-tag">{html_module.escape(str(tr))}</span>\n'
  564. else:
  565. html += f'<span class="detail-tag source-tag">{html_module.escape(str(time_range))}</span>\n'
  566. html += '</div>\n'
  567. html += '</div>\n'
  568. # 针对"抽象概念"(实质-抽象概念),显示"类型"、"来源"和"推理过程"
  569. # 注意:排除隐含概念(隐含概念有自己的显示逻辑)
  570. elif isinstance(dimension, dict) and dimension.get('二级') == '抽象概念':
  571. # 类型
  572. leixing = element.get('类型')
  573. if leixing:
  574. html += '<div class="detail-section">\n'
  575. html += '<strong>类型:</strong>\n'
  576. html += f'<div class="detail-text">{html_module.escape(str(leixing))}</div>\n'
  577. html += '</div>\n'
  578. # 来源(抽象概念的来源可能是复杂对象)
  579. laiyuan = element.get('来源')
  580. if laiyuan and isinstance(laiyuan, dict):
  581. html += '<div class="detail-section">\n'
  582. html += '<strong>来源:</strong>\n'
  583. html += '<div class="detail-content">\n'
  584. for key, values in laiyuan.items():
  585. if isinstance(values, list):
  586. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  587. for item in values:
  588. if isinstance(item, dict):
  589. item_id = item.get('id', '')
  590. item_name = item.get('名称', '')
  591. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  592. else:
  593. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  594. else:
  595. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  596. html += '</div>\n'
  597. html += '</div>\n'
  598. # 推理过程
  599. tuili_guocheng = element.get('推理过程')
  600. if tuili_guocheng:
  601. html += '<div class="detail-section">\n'
  602. html += '<strong>推理过程:</strong>\n'
  603. html += f'<div class="detail-text">{html_module.escape(tuili_guocheng)}</div>\n'
  604. html += '</div>\n'
  605. else:
  606. if source:
  607. html += '<div class="detail-section">\n'
  608. html += '<strong>来源:</strong>\n'
  609. html += '<div class="detail-content">\n'
  610. for src in source:
  611. html += f'<span class="detail-tag source-tag">{html_module.escape(str(src))}</span>\n'
  612. html += '</div>\n'
  613. html += '</div>\n'
  614. # 上下文验证(适用于具象概念)
  615. context_verification = element.get('上下文验证')
  616. if context_verification:
  617. html += '<div class="detail-section">\n'
  618. html += '<strong>上下文验证:</strong>\n'
  619. html += '<div class="context-verification" style="margin-top: 8px; padding: 12px; background-color: #f8f9fa; border-radius: 4px; border-left: 3px solid #6c757d;">\n'
  620. # 原文位置
  621. original_position = context_verification.get('原文位置', '')
  622. if original_position:
  623. html += '<div class="context-item" style="margin-bottom: 8px;">\n'
  624. html += '<strong style="color: #495057; font-size: 13px;">原文位置:</strong>\n'
  625. html += f'<div style="margin-top: 4px; padding: 6px 10px; background-color: #fff; border-radius: 3px; color: #212529; font-style: italic;">{html_module.escape(original_position)}</div>\n'
  626. html += '</div>\n'
  627. # 语法成分
  628. grammar_component = context_verification.get('语法成分', '')
  629. if grammar_component:
  630. html += '<div class="context-item" style="margin-bottom: 8px;">\n'
  631. html += '<strong style="color: #495057; font-size: 13px;">语法成分:</strong>\n'
  632. html += f'<span style="margin-left: 8px; padding: 3px 10px; background-color: #e7f3ff; color: #0066cc; border-radius: 3px; font-size: 12px;">{html_module.escape(grammar_component)}</span>\n'
  633. html += '</div>\n'
  634. # 语境判断
  635. context_judgment = context_verification.get('语境判断', '')
  636. if context_judgment:
  637. html += '<div class="context-item" style="margin-bottom: 0;">\n'
  638. html += '<strong style="color: #495057; font-size: 13px;">语境判断:</strong>\n'
  639. html += f'<div style="margin-top: 4px; padding: 8px 10px; background-color: #fff; border-radius: 3px; color: #495057; line-height: 1.6;">{html_module.escape(context_judgment)}</div>\n'
  640. html += '</div>\n'
  641. html += '</div>\n'
  642. html += '</div>\n'
  643. # 出现段落
  644. if paragraphs_list:
  645. html += '<div class="detail-section">\n'
  646. html += '<strong>出现段落:</strong>\n'
  647. html += '<div class="paragraphs-detail-list">\n'
  648. for para in paragraphs_list:
  649. if isinstance(para, dict):
  650. # 新结构:对象包含段落ID和如何体现
  651. para_id = para.get('段落ID', '')
  652. how = para.get('如何体现', '')
  653. html += '<div class="paragraph-detail-item">\n'
  654. html += f'<span class="detail-tag para-id-tag">{html_module.escape(para_id)}</span>\n'
  655. if how:
  656. html += f'<div class="para-how">{html_module.escape(how)}</div>\n'
  657. html += '</div>\n'
  658. else:
  659. # 旧结构:字符串
  660. html += f'<span class="detail-tag">{html_module.escape(str(para))}</span>\n'
  661. html += '</div>\n'
  662. html += '</div>\n'
  663. # 意图支撑
  664. if intent_support:
  665. html += '<div class="detail-section">\n'
  666. html += '<strong>意图支撑:</strong>\n'
  667. for point_type in ['灵感点', '目的点', '关键点']:
  668. if point_type in intent_support and intent_support[point_type]:
  669. html += f'<div class="score-type">{point_type}</div>\n'
  670. html += '<div class="score-list">\n'
  671. for item in intent_support[point_type]:
  672. point = item.get('点', '')
  673. point_intention = item.get('点的意图', '')
  674. support_reason = item.get('支撑理由', '')
  675. html += '<div class="score-item">\n'
  676. html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
  677. # 显示点的意图
  678. if point_intention:
  679. html += '<div class="point-intention">\n'
  680. html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
  681. html += '</div>\n'
  682. # 显示支撑理由
  683. if support_reason:
  684. html += '<div class="score-reasons">\n'
  685. html += f'<strong style="color: #666;">支撑理由:</strong>\n'
  686. html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
  687. html += '</div>\n'
  688. html += '</div>\n'
  689. html += '</div>\n'
  690. html += '</div>\n'
  691. # 权重明细(放在展开内容最底部,简化为紧凑样式)
  692. if weight_info.get('raw_total', 0) > 0:
  693. wd = weight_info['details']
  694. ss = weight_info['support_stats']
  695. # 判断是形式元素还是实质元素
  696. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  697. html += '<div class="detail-section weight-detail-section" style="margin-top: 12px; border-top: 1px dashed #e0e0e0; padding-top: 10px;">\n'
  698. html += '<strong style="font-size: 13px; color: #555;">权重得分明细:</strong>\n'
  699. html += '<div class="weight-summary-header" style="margin-top: 4px; font-size: 12px; color: #666;">\n'
  700. if is_form:
  701. html += f'权重分:{weight_info["weight_score"]:.1f}(形式元素使用新的权重计算逻辑)\n'
  702. else:
  703. html += f'原始总分:{weight_info["raw_total"]:.1f},权重分:{weight_info["weight_score"]:.1f}(min(100, 原始总分 × 100 / 110))\n'
  704. html += '</div>\n'
  705. html += '<div class="weight-summary-grid" style="margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px;">\n'
  706. # 共性维度得分
  707. if is_form:
  708. # 形式元素:显示频次分、覆盖段落数分、覆盖率分
  709. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">频次分 {wd.get("频次分", 0):.1f}</span>\n'
  710. if "覆盖段落数分" in wd:
  711. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">覆盖段落数分 {wd.get("覆盖段落数分", 0):.1f}</span>\n'
  712. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">覆盖率分 {wd.get("覆盖率分", 0):.1f}</span>\n'
  713. else:
  714. # 实质元素:显示频次分、覆盖率分
  715. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">频次分 {wd.get("频次分", 0):.1f} / 30</span>\n'
  716. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">覆盖率分 {wd.get("覆盖率分", 0):.1f} / 30</span>\n'
  717. # 支撑维度得分
  718. inspiration_count = ss.get('灵感点数量', 0)
  719. if is_form:
  720. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f}({inspiration_count} 个)</span>\n'
  721. else:
  722. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f} / 25({inspiration_count} 个)</span>\n'
  723. purpose_count = ss.get('目的点数量', 0)
  724. if is_form:
  725. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">目的点支撑分 {wd.get("目的点支撑分", 0):.1f}({purpose_count} 个)</span>\n'
  726. else:
  727. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">目的点支撑分 {wd.get("目的点支撑分", 0):.1f} / 15({purpose_count} 个)</span>\n'
  728. keypoint_count = ss.get('关键点数量', 0)
  729. if is_form:
  730. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">关键点支撑分 {wd.get("关键点支撑分", 0):.1f}({keypoint_count} 个)</span>\n'
  731. else:
  732. html += f'<span class="weight-chip" style="font-size: 12px; padding: 2px 8px; border-radius: 10px; background: #f5f5f5; color: #333;">关键点支撑分 {wd.get("关键点支撑分", 0):.1f} / 10({keypoint_count} 个)</span>\n'
  733. html += '</div>\n'
  734. html += '</div>\n'
  735. html += '</div>\n'
  736. html += '</li>\n'
  737. return html
  738. def generate_tab3_content(data: Dict[str, Any]) -> str:
  739. """生成Tab3内容:按层次展示(实质/形式 → 具体元素/具体概念/抽象概念 → 树形展示)"""
  740. html = '<div class="tab-content" id="tab3" style="display:none;">\n'
  741. # 添加全局控制按钮
  742. html += '<div class="global-controls">\n'
  743. html += ' <div class="color-legend">\n'
  744. html += ' <span class="legend-title">颜色图例(主导因素):</span>\n'
  745. html += ' <span class="legend-item legend-coverage">覆盖率</span>\n'
  746. html += ' <span class="legend-item legend-frequency">频次</span>\n'
  747. html += ' <span class="legend-item legend-intent">意图支撑</span>\n'
  748. html += ' </div>\n'
  749. html += ' <button class="control-btn" onclick="toggleAllLevels(true)">全部展开</button>\n'
  750. html += ' <button class="control-btn" onclick="toggleAllLevels(false)">全部收起</button>\n'
  751. html += '</div>\n'
  752. if '脚本理解' in data:
  753. script = data['脚本理解']
  754. # 尝试获取元素列表,如果不存在则合并实质列表和形式列表
  755. elements = script.get('元素列表', [])
  756. if not elements:
  757. substance_list = script.get('实质列表', [])
  758. form_list = script.get('形式列表', [])
  759. elements = substance_list + form_list
  760. # 第一层:按维度.一级分组(实质 vs 形式)
  761. level1_groups = {}
  762. for elem in elements:
  763. dimension = elem.get('维度', {})
  764. if isinstance(dimension, dict):
  765. level1 = dimension.get('一级', '实质')
  766. else:
  767. # 兼容旧结构
  768. level1 = elem.get('类型', '实质')
  769. if level1 not in level1_groups:
  770. level1_groups[level1] = []
  771. level1_groups[level1].append(elem)
  772. # 按顺序渲染:实质、形式
  773. for level1_name in ['实质', '形式']:
  774. if level1_name not in level1_groups:
  775. continue
  776. level1_elements = level1_groups[level1_name]
  777. html += '<div class="section level1-section">\n'
  778. html += f'<div class="level1-header collapsible" onclick="toggleLevel1(this)">\n'
  779. html += '<span class="level-toggle-icon">▼</span>\n'
  780. html += f'<h2 class="level1-title">{level1_name} ({len(level1_elements)}个)</h2>\n'
  781. html += '</div>\n'
  782. html += '<div class="level1-content">\n'
  783. # 第二层:按维度.二级分组
  784. level2_groups = {}
  785. for elem in level1_elements:
  786. dimension = elem.get('维度', {})
  787. elem_type = elem.get('类型', '')
  788. # 隐含概念:优先通过类型判断(因为维度二级可能是"隐含概念"或"抽象概念")
  789. if elem_type == '隐含概念':
  790. level2 = '隐含概念'
  791. elif isinstance(dimension, dict):
  792. level2 = dimension.get('二级', '具体元素')
  793. else:
  794. # 兼容旧结构
  795. elem_type_old = elem.get('类型', '实质')
  796. if elem_type_old == '实质':
  797. level2 = '具体元素'
  798. elif elem_type_old == '具象概念':
  799. level2 = '具体概念'
  800. else:
  801. level2 = '抽象概念'
  802. if level2 not in level2_groups:
  803. level2_groups[level2] = []
  804. level2_groups[level2].append(elem)
  805. # 根据一级维度确定二级维度遍历顺序
  806. if level1_name == '实质':
  807. level2_order = ['具体元素', '具象概念', '隐含概念', '抽象概念']
  808. else: # 形式
  809. level2_order = ['具体元素形式', '具象概念形式', '整体形式']
  810. # 按顺序渲染二级维度
  811. for level2_name in level2_order:
  812. if level2_name not in level2_groups:
  813. continue
  814. level2_elements = level2_groups[level2_name]
  815. html += '<div class="level2-section">\n'
  816. html += f'<div class="level2-header collapsible" onclick="toggleLevel2(this)">\n'
  817. html += '<span class="level-toggle-icon">▼</span>\n'
  818. html += f'<h3 class="level2-title">{level2_name} ({len(level2_elements)}个)</h3>\n'
  819. html += '</div>\n'
  820. html += '<div class="level2-content">\n'
  821. # 第三层:按树形分类结构组织
  822. hierarchical_categories = group_elements_by_hierarchical_category(level2_elements)
  823. # 渲染树形分类结构
  824. for cat_level1_name, cat_level1_data in hierarchical_categories.items():
  825. # 收集该一级分类下的所有元素
  826. all_cat_elements = cat_level1_data['elements'][:]
  827. for level2_elems in cat_level1_data['level2_groups'].values():
  828. all_cat_elements.extend(level2_elems)
  829. if not all_cat_elements:
  830. continue
  831. # 计算一级分类的统计信息
  832. avg_coverage = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in all_cat_elements) / len(all_cat_elements)
  833. avg_intent_count = sum(calculate_intent_support_count(e) for e in all_cat_elements) / len(all_cat_elements)
  834. html += '<div class="category-group collapsible">\n'
  835. html += '<div class="category-header" onclick="toggleCategoryGroup(this)">\n'
  836. html += '<span class="category-toggle-icon">▼</span>\n'
  837. html += f'<h4 class="category-title">{html_module.escape(cat_level1_name)} ({len(all_cat_elements)}个)</h4>\n'
  838. html += '<div class="category-stats">\n'
  839. html += f'<span class="stat-badge">平均覆盖率: {avg_coverage:.2%}</span>\n'
  840. html += f'<span class="stat-badge">平均意图支撑: {avg_intent_count:.1f}</span>\n'
  841. html += '</div>\n'
  842. html += '</div>\n'
  843. html += '<div class="category-content">\n'
  844. # 渲染一级分类直接包含的元素
  845. if cat_level1_data['elements']:
  846. html += '<ul class="element-list">\n'
  847. for elem in cat_level1_data['elements']:
  848. html += render_element_item(elem, all_cat_elements)
  849. html += '</ul>\n'
  850. # 渲染二级分类
  851. for cat_level2_name, cat_level2_elements in cat_level1_data['level2_groups'].items():
  852. if not cat_level2_elements:
  853. continue
  854. # 计算二级分类的统计信息
  855. avg_coverage_l2 = sum((e.get('共性分析') or {}).get('段落覆盖率', 0.0) for e in cat_level2_elements) / len(cat_level2_elements)
  856. avg_intent_count_l2 = sum(calculate_intent_support_count(e) for e in cat_level2_elements) / len(cat_level2_elements)
  857. html += '<div class="subcategory-group collapsible">\n'
  858. html += '<div class="subcategory-header" onclick="toggleSubcategoryGroup(this)">\n'
  859. html += '<span class="subcategory-toggle-icon">▼</span>\n'
  860. html += f'<h5 class="subcategory-title">{html_module.escape(cat_level2_name)} ({len(cat_level2_elements)}个)</h5>\n'
  861. html += '<div class="subcategory-stats">\n'
  862. html += f'<span class="stat-badge-small">覆盖率: {avg_coverage_l2:.2%}</span>\n'
  863. html += '</div>\n'
  864. html += '</div>\n'
  865. html += '<div class="subcategory-content">\n'
  866. html += '<ul class="element-list">\n'
  867. for elem in cat_level2_elements:
  868. html += render_element_item(elem, cat_level2_elements)
  869. html += '</ul>\n'
  870. html += '</div>\n'
  871. html += '</div>\n'
  872. html += '</div>\n'
  873. html += '</div>\n'
  874. html += '</div>\n' # level2-content
  875. html += '</div>\n' # level2-section
  876. html += '</div>\n' # level1-content
  877. html += '</div>\n' # level1-section
  878. html += '</div>\n'
  879. return html