tab3.py 40 KB

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