tab3.py 77 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926
  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_element_coverage(element: Dict[str, Any]) -> float:
  47. """
  48. 获取元素的段落覆盖率(兼容形式元素和实质元素)
  49. 对于实质元素:从"共性分析"字段获取
  50. 对于形式元素:从"权重明细"中的"覆盖率分"反推
  51. 根据script_form_extraction_agent.py的逻辑:
  52. - coverage_rate_base_score = coverage_rate * 50 (基础分0-50)
  53. - coverage_rate_score = coverage_rate_base_score * 0.3 (加权后0-15)
  54. 所以:coverage_rate = coverage_rate_score / 15
  55. Args:
  56. element: 元素数据
  57. Returns:
  58. 段落覆盖率(0.0-1.0)
  59. """
  60. dimension = element.get('维度', {})
  61. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  62. if is_form:
  63. # 形式元素:从权重明细中反推覆盖率
  64. weight_details = element.get('权重明细', {})
  65. if weight_details:
  66. coverage_score = float(weight_details.get('覆盖率分', 0) or 0)
  67. # 覆盖率分 = 覆盖率 × 50 × 0.3 = 覆盖率 × 15
  68. # 所以:覆盖率 = 覆盖率分 / 15
  69. if coverage_score > 0:
  70. coverage = coverage_score / 15.0
  71. return min(1.0, max(0.0, coverage))
  72. # 如果权重明细中没有覆盖率分,返回0
  73. return 0.0
  74. else:
  75. # 实质元素:从共性分析中获取
  76. commonality = element.get('共性分析') or {}
  77. coverage = float(commonality.get('段落覆盖率', 0.0) or 0.0)
  78. return coverage
  79. def get_support_stats(element: Dict[str, Any]) -> Dict[str, int]:
  80. """
  81. 获取元素的支撑统计信息(灵感点/目的点/关键点数量)
  82. 优先使用元素中已经预计算好的 support_stats 字段;
  83. 若不存在,则根据「意图支撑」或「多维度评分」字段动态统计。
  84. """
  85. # 优先使用预计算的支撑统计
  86. support_stats = element.get('支撑统计')
  87. if isinstance(support_stats, dict):
  88. # 做一次安全拷贝并补齐缺失字段
  89. return {
  90. '灵感点数量': int(support_stats.get('灵感点数量', 0) or 0),
  91. '目的点数量': int(support_stats.get('目的点数量', 0) or 0),
  92. '关键点数量': int(support_stats.get('关键点数量', 0) or 0),
  93. }
  94. # 兼容旧字段名
  95. support_stats_old = element.get('support_stats')
  96. if isinstance(support_stats_old, dict):
  97. return {
  98. '灵感点数量': int(support_stats_old.get('灵感点数量', 0) or 0),
  99. '目的点数量': int(support_stats_old.get('目的点数量', 0) or 0),
  100. '关键点数量': int(support_stats_old.get('关键点数量', 0) or 0),
  101. }
  102. # 统一使用意图支撑数据统计(实质和形式都使用相同逻辑)
  103. # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  104. intent_support_data = get_intent_support_data(element)
  105. return {
  106. '灵感点数量': len(intent_support_data.get('灵感点', []) or []),
  107. '目的点数量': len(intent_support_data.get('目的点', []) or []),
  108. '关键点数量': len(intent_support_data.get('关键点', []) or []),
  109. }
  110. def compute_weight_scores(element: Dict[str, Any]) -> Dict[str, Any]:
  111. """
  112. 计算元素的权重相关得分。
  113. 对于形式元素:优先使用元素中预计算的权重分和权重明细
  114. 对于实质元素:根据共性分析和意图支撑动态计算
  115. 原始总分 = 各子项得分之和
  116. 权重分 = min(100, 原始总分 × 100 / 110)
  117. """
  118. # 判断是形式元素还是实质元素
  119. dimension = element.get('维度') or {}
  120. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  121. # 形式元素:优先使用预计算的权重信息
  122. if is_form:
  123. weight_score = element.get('权重分')
  124. weight_details = element.get('权重明细')
  125. support_stats = get_support_stats(element)
  126. # 如果存在预计算的权重信息,直接使用
  127. if weight_score is not None and weight_details is not None:
  128. # 从权重明细中提取各项得分
  129. freq_score = float(weight_details.get('频次分', 0) or 0)
  130. coverage_count_score = float(weight_details.get('覆盖段落数分', 0) or 0)
  131. coverage_rate_score = float(weight_details.get('覆盖率分', 0) or 0)
  132. inspiration_score = float(weight_details.get('灵感点支撑分', 0) or 0)
  133. purpose_score = float(weight_details.get('目的点支撑分', 0) or 0)
  134. keypoint_score = float(weight_details.get('关键点支撑分', 0) or 0)
  135. # 计算原始总分(共性总分 + 支撑总分)
  136. commonality_total = weight_details.get('共性总分', 0) or 0
  137. support_total = weight_details.get('支撑总分', 0) or 0
  138. raw_total = float(commonality_total) + float(support_total)
  139. return {
  140. 'weight_score': float(weight_score),
  141. 'raw_total': raw_total,
  142. 'details': {
  143. '频次分': freq_score,
  144. '覆盖段落数分': coverage_count_score,
  145. '覆盖率分': coverage_rate_score,
  146. '灵感点支撑分': inspiration_score,
  147. '目的点支撑分': purpose_score,
  148. '关键点支撑分': keypoint_score,
  149. },
  150. 'support_stats': support_stats,
  151. }
  152. # 实质元素或形式元素没有预计算权重:使用旧的计算逻辑
  153. commonality = element.get('共性分析') or {}
  154. coverage = float(commonality.get('段落覆盖率', 0.0) or 0.0)
  155. frequency = int(commonality.get('出现频次', 0) or 0)
  156. support_stats = get_support_stats(element)
  157. inspiration_count = support_stats.get('灵感点数量', 0) or 0
  158. purpose_count = support_stats.get('目的点数量', 0) or 0
  159. keypoint_count = support_stats.get('关键点数量', 0) or 0
  160. # 1) 频次分(0–30分)
  161. # 假定「高频」的参考上限为 12 次,超过即视为满分
  162. # 频次分 = min(30, 出现频次 / 12 * 30)
  163. if frequency <= 0:
  164. freq_score = 0.0
  165. else:
  166. freq_score = min(30.0, frequency * 30.0 / 12.0)
  167. # 2) 覆盖率分(0–30分)
  168. # 覆盖率分 = 段落覆盖率 × 30
  169. coverage_score = max(0.0, min(30.0, coverage * 30.0))
  170. # 3) 灵感点支撑分(0–25分)
  171. # 按你的说明:支撑{灵感点数量}个灵感点 × 25分/个,封顶 25 分
  172. inspiration_score = min(25.0, float(inspiration_count) * 25.0)
  173. # 4) 目的点支撑分(0–15分)
  174. # 说明:支撑{目的点数量}个目的点 × 5分/个,封顶 15 分
  175. purpose_score = min(15.0, float(purpose_count) * 5.0)
  176. # 5) 关键点支撑分(0–10分)
  177. # 说明:支撑{关键点数量}个关键点 × 1分/个,封顶 10 分
  178. keypoint_score = min(10.0, float(keypoint_count) * 1.0)
  179. raw_total = freq_score + coverage_score + inspiration_score + purpose_score + keypoint_score
  180. if raw_total <= 0:
  181. weight_score = 0.0
  182. else:
  183. weight_score = min(100.0, raw_total * 100.0 / 110.0)
  184. return {
  185. 'weight_score': weight_score,
  186. 'raw_total': raw_total,
  187. 'details': {
  188. '频次分': freq_score,
  189. '覆盖率分': coverage_score,
  190. '灵感点支撑分': inspiration_score,
  191. '目的点支撑分': purpose_score,
  192. '关键点支撑分': keypoint_score,
  193. },
  194. 'support_stats': support_stats,
  195. }
  196. def determine_dominant_factor(element: Dict[str, Any], all_elements: List[Dict[str, Any]]) -> str:
  197. """
  198. 判断元素排序的主导因素
  199. 排序规则:覆盖率 > 频次 > 意图支撑数
  200. 主导因素判断:哪个指标在当前元素中相对最显著
  201. Args:
  202. element: 当前元素
  203. all_elements: 同组所有元素
  204. Returns:
  205. 主导因素: 'coverage' | 'frequency' | 'intent_support'
  206. """
  207. if not all_elements:
  208. return 'coverage'
  209. # 获取当前元素的指标
  210. commonality = element.get('共性分析') or {}
  211. coverage = commonality.get('段落覆盖率', 0.0)
  212. frequency = commonality.get('出现频次', 0)
  213. intent_count = calculate_intent_support_count(element)
  214. # 将所有指标归一化到同一量级,然后比较
  215. # 覆盖率已经是0-1范围
  216. # 频次归一化:假设最大频次为10
  217. normalized_frequency = min(frequency / 10.0, 1.0)
  218. # 意图支撑数归一化:假设最大支撑数为10
  219. normalized_intent = min(intent_count / 10.0, 1.0)
  220. # 比较归一化后的值,取最大的作为主导因素
  221. scores = {
  222. 'coverage': coverage,
  223. 'frequency': normalized_frequency,
  224. 'intent_support': normalized_intent
  225. }
  226. return max(scores, key=scores.get)
  227. def sort_elements_by_coverage_and_frequency(elements: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
  228. """
  229. 按照覆盖率、频次和意图支撑数对元素排序
  230. 排序规则:
  231. 1. 第一优先级:共性(段落覆盖率)- 倒序
  232. 2. 第二优先级:共性(出现频次)- 倒序
  233. 3. 第三优先级:意图支撑数 - 倒序
  234. Args:
  235. elements: 元素列表
  236. Returns:
  237. 排序后的元素列表
  238. """
  239. def get_sort_key(elem):
  240. # 获取共性分析,防止为None
  241. commonality = elem.get('共性分析') or {}
  242. # 获取段落覆盖率
  243. coverage = commonality.get('段落覆盖率', 0.0)
  244. # 获取出现频次
  245. frequency = commonality.get('出现频次', 0)
  246. # 计算意图支撑数
  247. intent_count = calculate_intent_support_count(elem)
  248. # 返回排序键(负数用于倒序)
  249. return (-coverage, -frequency, -intent_count)
  250. return sorted(elements, key=get_sort_key)
  251. def get_element_category(element: Dict[str, Any]) -> str:
  252. """
  253. 获取元素的分类名称(支持新旧两种数据结构)
  254. Args:
  255. element: 元素数据
  256. Returns:
  257. 分类名称字符串
  258. """
  259. category = element.get('分类', '未分类')
  260. if isinstance(category, dict):
  261. # 新结构:分类是对象,包含一级分类和二级分类
  262. level1 = category.get('一级分类', '')
  263. level2 = category.get('二级分类', '')
  264. if level1 and level2:
  265. return f"{level1} - {level2}"
  266. elif level1:
  267. return level1
  268. else:
  269. return '未分类'
  270. else:
  271. # 旧结构:分类是字符串或列表
  272. if isinstance(category, list):
  273. return ' - '.join(category) if category else '未分类'
  274. return category if category else '未分类'
  275. def group_elements_by_hierarchical_category(elements: List[Dict[str, Any]]) -> Dict[str, Any]:
  276. """
  277. 按树形分类结构组织元素(一级分类 → 二级分类 → 元素)
  278. 优化规则:同一个父节点下的所有子节点采用统一的分类格式展示
  279. - 如果一级分类下既有元素又有分类,将元素归入"未分类"二级分类
  280. Args:
  281. elements: 元素列表
  282. Returns:
  283. 树形分类结构字典
  284. """
  285. # 1. 按一级分类和二级分类分组
  286. level1_groups = {}
  287. for elem in elements:
  288. category_data = elem.get('分类', {})
  289. if isinstance(category_data, dict):
  290. level1 = category_data.get('一级分类', '未分类')
  291. level2 = category_data.get('二级分类', '')
  292. elif isinstance(category_data, list):
  293. # 列表格式:第一个元素作为一级分类,第二个作为二级分类
  294. level1 = category_data[0] if len(category_data) > 0 else '未分类'
  295. level2 = category_data[1] if len(category_data) > 1 else ''
  296. else:
  297. # 旧结构:分类是字符串
  298. level1 = str(category_data) if category_data else '未分类'
  299. level2 = ''
  300. # 初始化一级分类
  301. if level1 not in level1_groups:
  302. level1_groups[level1] = {
  303. 'elements': [],
  304. 'level2_groups': {}
  305. }
  306. # 如果有二级分类,放入二级分类组;否则放入一级分类的直接元素列表(临时)
  307. if level2:
  308. if level2 not in level1_groups[level1]['level2_groups']:
  309. level1_groups[level1]['level2_groups'][level2] = []
  310. level1_groups[level1]['level2_groups'][level2].append(elem)
  311. else:
  312. level1_groups[level1]['elements'].append(elem)
  313. # 1.5 优化:仅当一级分类下既有直接元素又有二级分类时,才将直接元素移到"未分类"二级分类中
  314. # 如果一级分类下只有直接元素,没有二级分类,则保持原样(不需要"未分类"概念)
  315. for level1_name, level1_data in level1_groups.items():
  316. if level1_data['elements'] and level1_data['level2_groups']:
  317. # 将直接元素移到"未分类"分类
  318. if '未分类' not in level1_data['level2_groups']:
  319. level1_data['level2_groups']['未分类'] = []
  320. level1_data['level2_groups']['未分类'].extend(level1_data['elements'])
  321. level1_data['elements'] = []
  322. # 如果只有直接元素,没有二级分类,则保持level1_data['elements']不变
  323. # 2. 对每个分类内的元素排序
  324. for level1_data in level1_groups.values():
  325. # 排序一级分类直接包含的元素
  326. if level1_data['elements']:
  327. level1_data['elements'] = sort_elements_by_coverage_and_frequency(level1_data['elements'])
  328. # 排序每个二级分类的元素
  329. for level2_name in level1_data['level2_groups']:
  330. level1_data['level2_groups'][level2_name] = sort_elements_by_coverage_and_frequency(
  331. level1_data['level2_groups'][level2_name]
  332. )
  333. # 3. 计算每个一级分类的统计信息用于排序
  334. level1_scores = {}
  335. for level1_name, level1_data in level1_groups.items():
  336. # 收集该一级分类下的所有元素(包括二级分类下的)
  337. all_elements = level1_data['elements'][:]
  338. for level2_elements in level1_data['level2_groups'].values():
  339. all_elements.extend(level2_elements)
  340. if not all_elements:
  341. level1_scores[level1_name] = (0.0, 0, 0.0)
  342. continue
  343. # 计算统计指标
  344. avg_coverage = sum(get_element_coverage(e) for e in all_elements) / len(all_elements)
  345. avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in all_elements) / len(all_elements)
  346. avg_intent_count = sum(calculate_intent_support_count(e) for e in all_elements) / len(all_elements)
  347. level1_scores[level1_name] = (avg_coverage, avg_frequency, avg_intent_count)
  348. # 4. 对一级分类排序
  349. sorted_level1 = sorted(
  350. level1_scores.keys(),
  351. key=lambda c: (-level1_scores[c][0], -level1_scores[c][1], -level1_scores[c][2])
  352. )
  353. # 5. 对每个一级分类内的二级分类排序
  354. for level1_name in sorted_level1:
  355. level1_data = level1_groups[level1_name]
  356. level2_groups = level1_data['level2_groups']
  357. if not level2_groups:
  358. continue
  359. # 计算二级分类的统计信息
  360. level2_scores = {}
  361. for level2_name, level2_elements in level2_groups.items():
  362. if not level2_elements:
  363. level2_scores[level2_name] = (0.0, 0, 0.0)
  364. continue
  365. avg_coverage = sum(get_element_coverage(e) for e in level2_elements) / len(level2_elements)
  366. avg_frequency = sum((e.get('共性分析') or {}).get('出现频次', 0) for e in level2_elements) / len(level2_elements)
  367. avg_intent_count = sum(calculate_intent_support_count(e) for e in level2_elements) / len(level2_elements)
  368. level2_scores[level2_name] = (avg_coverage, avg_frequency, avg_intent_count)
  369. # 排序二级分类
  370. sorted_level2_names = sorted(
  371. level2_scores.keys(),
  372. key=lambda c: (-level2_scores[c][0], -level2_scores[c][1], -level2_scores[c][2])
  373. )
  374. # 重新组织为有序字典
  375. sorted_level2_groups = {name: level2_groups[name] for name in sorted_level2_names}
  376. level1_data['level2_groups'] = sorted_level2_groups
  377. # 6. 返回排序后的结构
  378. return {level1_name: level1_groups[level1_name] for level1_name in sorted_level1}
  379. def render_element_item(element: Dict[str, Any], all_elements: List[Dict[str, Any]] = None) -> str:
  380. """渲染单个元素项的HTML(卡片样式,详细信息在弹窗中)
  381. Args:
  382. element: 元素数据
  383. all_elements: 同组所有元素(用于计算主导因素)
  384. """
  385. elem_id = element.get('id', '')
  386. name = element.get('名称') or '' # 处理None的情况
  387. description = element.get('描述') or '' # 处理None的情况
  388. # 获取类型和维度(兼容新旧结构)
  389. dimension = element.get('维度', {})
  390. if isinstance(dimension, dict):
  391. elem_type = dimension.get('一级', '')
  392. elem_type_level2 = dimension.get('二级', '')
  393. else:
  394. elem_type = element.get('类型', '')
  395. elem_type_level2 = ''
  396. # 获取共性分析(防止为None)
  397. commonality = element.get('共性分析') or {}
  398. coverage = commonality.get('段落覆盖率', 0.0)
  399. frequency = commonality.get('出现频次', 0)
  400. intent_count = calculate_intent_support_count(element)
  401. # 计算权重得分
  402. weight_info = compute_weight_scores(element)
  403. # 计算主导因素
  404. dominant_factor = 'coverage' # 默认
  405. if all_elements:
  406. dominant_factor = determine_dominant_factor(element, all_elements)
  407. # 根据主导因素确定边框颜色
  408. border_color_class = f'dominant-{dominant_factor}'
  409. # 检查是否有详细信息
  410. category_def = element.get('分类定义', '')
  411. paragraphs_list = commonality.get('出现段落列表', [])
  412. source = element.get('来源', [])
  413. intent_support = get_intent_support_data(element)
  414. has_details = bool(category_def or paragraphs_list or source or intent_support or weight_info.get('raw_total', 0) > 0)
  415. # 判断是否为形式元素
  416. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  417. # 卡片样式
  418. html = f'<li class="element-card {border_color_class}" data-elem-id="{elem_id}">\n'
  419. html += '<div class="element-card-body">\n'
  420. # 卡片头部
  421. html += '<div class="element-card-header">\n'
  422. if elem_id:
  423. html += f'<span class="element-card-id">#{elem_id}</span>\n'
  424. html += f'<h4 class="element-card-name">{html_module.escape(name)}</h4>\n'
  425. html += '</div>\n'
  426. # 统计指标
  427. html += '<div class="element-card-stats">\n'
  428. if weight_info.get('raw_total', 0) > 0:
  429. html += f'<span class="stat-badge stat-weight">权重分: {weight_info["weight_score"]:.1f}</span>\n'
  430. if is_form:
  431. html += f'<span class="stat-badge stat-intent">支撑: {intent_count}</span>\n'
  432. else:
  433. coverage_highlight = 'stat-highlight' if dominant_factor == 'coverage' else ''
  434. frequency_highlight = 'stat-highlight' if dominant_factor == 'frequency' else ''
  435. intent_highlight = 'stat-highlight' if dominant_factor == 'intent_support' else ''
  436. html += f'<span class="stat-badge stat-coverage {coverage_highlight}">覆盖率: {coverage:.2%}</span>\n'
  437. html += f'<span class="stat-badge stat-frequency {frequency_highlight}">频次: {frequency}</span>\n'
  438. html += f'<span class="stat-badge stat-intent {intent_highlight}">意图支撑: {intent_count}</span>\n'
  439. html += '</div>\n'
  440. # 描述
  441. if description:
  442. html += f'<div class="element-card-description">{html_module.escape(description)}</div>\n'
  443. # 查看详情按钮
  444. if has_details:
  445. html += f'<button class="element-detail-btn" onclick="openElementModal(\'{elem_id}\')">查看详情</button>\n'
  446. html += '</div>\n' # element-card-body
  447. html += '</li>\n'
  448. return html
  449. def render_element_modal(element: Dict[str, Any]) -> str:
  450. """渲染元素详情的弹窗内容
  451. Args:
  452. element: 元素数据
  453. """
  454. elem_id = element.get('id', '')
  455. name = element.get('名称') or ''
  456. description = element.get('描述') or ''
  457. # 获取类型和维度
  458. dimension = element.get('维度', {})
  459. if isinstance(dimension, dict):
  460. elem_type = dimension.get('一级', '')
  461. elem_type_level2 = dimension.get('二级', '')
  462. else:
  463. elem_type = element.get('类型', '')
  464. elem_type_level2 = ''
  465. # 获取分类
  466. category_data = element.get('分类', '')
  467. category_def = element.get('分类定义', '')
  468. # 获取共性分析
  469. commonality = element.get('共性分析') or {}
  470. coverage = commonality.get('段落覆盖率', 0.0)
  471. frequency = commonality.get('出现频次', 0)
  472. paragraphs_list = commonality.get('出现段落列表', [])
  473. source = element.get('来源', [])
  474. intent_support = get_intent_support_data(element)
  475. weight_info = compute_weight_scores(element)
  476. html = f'<div class="element-modal-content" data-elem-id="{elem_id}">\n'
  477. html += '<div class="modal-header">\n'
  478. if elem_id:
  479. html += f'<span class="modal-id">#{elem_id}</span>\n'
  480. html += f'<h3 class="modal-title">{html_module.escape(name)}</h3>\n'
  481. html += '<button class="modal-close" onclick="closeElementModal()">&times;</button>\n'
  482. html += '</div>\n'
  483. html += '<div class="modal-body">\n'
  484. # 描述
  485. if description:
  486. html += '<div class="detail-section">\n'
  487. html += '<strong>描述:</strong>\n'
  488. html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
  489. html += '</div>\n'
  490. # 分类定义
  491. if category_def:
  492. html += '<div class="detail-section">\n'
  493. html += '<strong>分类定义:</strong>\n'
  494. html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
  495. html += '</div>\n'
  496. # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
  497. if isinstance(dimension, dict) and dimension.get('一级') == '形式':
  498. # 支撑
  499. zhicheng = element.get('支撑')
  500. if zhicheng:
  501. html += '<div class="detail-section">\n'
  502. html += '<strong>支撑:</strong>\n'
  503. html += '<div class="detail-content">\n'
  504. if isinstance(zhicheng, list):
  505. for item in zhicheng:
  506. if isinstance(item, dict):
  507. item_id = item.get('id', '')
  508. item_name = item.get('名称', '')
  509. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  510. else:
  511. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  512. elif isinstance(zhicheng, dict):
  513. # 支撑可能是对象(包含具体元素、具象概念等)
  514. for key, values in zhicheng.items():
  515. if isinstance(values, list):
  516. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  517. for item in values:
  518. if isinstance(item, dict):
  519. item_id = item.get('id', '')
  520. item_name = item.get('名称', '')
  521. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  522. else:
  523. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  524. else:
  525. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  526. else:
  527. html += f'<span class="detail-tag">{html_module.escape(str(zhicheng))}</span>\n'
  528. html += '</div>\n'
  529. html += '</div>\n'
  530. # 推理
  531. tuili = element.get('推理')
  532. if tuili:
  533. html += '<div class="detail-section">\n'
  534. html += '<strong>推理:</strong>\n'
  535. html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
  536. html += '</div>\n'
  537. # 支撑关系(形式元素专用,从意图支撑数据中显示支撑点)
  538. # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  539. intent_support_data = get_intent_support_data(element)
  540. if intent_support_data:
  541. html += '<div class="detail-section">\n'
  542. html += '<strong>支撑关系:</strong>\n'
  543. for point_type in ['灵感点', '目的点', '关键点']:
  544. support_points = intent_support_data.get(point_type) or []
  545. if not support_points:
  546. continue
  547. html += f'<div class="score-type">{point_type}</div>\n'
  548. html += '<div class="score-list">\n'
  549. # 每个项目就是一个支撑点
  550. for support_point in support_points:
  551. if not isinstance(support_point, dict):
  552. continue
  553. point = support_point.get('点', '')
  554. point_intention = support_point.get('点的意图', '')
  555. support_reason = support_point.get('支撑理由', '')
  556. html += '<div class="score-item">\n'
  557. html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
  558. # 显示点的意图
  559. if point_intention:
  560. html += '<div class="point-intention">\n'
  561. html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
  562. html += '</div>\n'
  563. # 显示支撑理由
  564. if support_reason:
  565. html += '<div class="score-reasons">\n'
  566. html += f'<strong style="color: #666;">支撑理由:</strong>\n'
  567. html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
  568. html += '</div>\n'
  569. html += '</div>\n'
  570. html += '</div>\n' # end score-list
  571. html += '</div>\n' # end detail-section
  572. # 针对"隐含概念",显示"来源"(声音特征、语气语调、BGM、音效等)和"时间范围"
  573. elem_type = element.get('类型', '')
  574. if elem_type == '隐含概念':
  575. # 来源(隐含概念的来源包含声音特征、语气语调、背景音乐、音效等)
  576. laiyuan = element.get('来源')
  577. if laiyuan and isinstance(laiyuan, dict):
  578. html += '<div class="detail-section">\n'
  579. html += '<strong>来源:</strong>\n'
  580. html += '<div class="detail-content">\n'
  581. for key, values in laiyuan.items():
  582. if isinstance(values, list) and values:
  583. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  584. for item in values:
  585. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  586. elif values:
  587. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  588. html += '</div>\n'
  589. html += '</div>\n'
  590. # 时间范围
  591. time_range = element.get('时间范围')
  592. if time_range:
  593. html += '<div class="detail-section">\n'
  594. html += '<strong>时间范围:</strong>\n'
  595. html += '<div class="detail-content">\n'
  596. if isinstance(time_range, list):
  597. for tr in time_range:
  598. html += f'<span class="detail-tag source-tag">{html_module.escape(str(tr))}</span>\n'
  599. else:
  600. html += f'<span class="detail-tag source-tag">{html_module.escape(str(time_range))}</span>\n'
  601. html += '</div>\n'
  602. html += '</div>\n'
  603. # 针对"抽象概念"(实质-抽象概念),显示"类型"、"来源"和"推理过程"
  604. # 注意:排除隐含概念(隐含概念有自己的显示逻辑)
  605. elif isinstance(dimension, dict) and dimension.get('二级') == '抽象概念':
  606. # 类型
  607. leixing = element.get('类型')
  608. if leixing:
  609. html += '<div class="detail-section">\n'
  610. html += '<strong>类型:</strong>\n'
  611. html += f'<div class="detail-text">{html_module.escape(str(leixing))}</div>\n'
  612. html += '</div>\n'
  613. # 来源(抽象概念的来源可能是复杂对象)
  614. laiyuan = element.get('来源')
  615. if laiyuan and isinstance(laiyuan, dict):
  616. html += '<div class="detail-section">\n'
  617. html += '<strong>来源:</strong>\n'
  618. html += '<div class="detail-content">\n'
  619. for key, values in laiyuan.items():
  620. if isinstance(values, list):
  621. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  622. for item in values:
  623. if isinstance(item, dict):
  624. item_id = item.get('id', '')
  625. item_name = item.get('名称', '')
  626. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  627. else:
  628. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  629. else:
  630. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  631. html += '</div>\n'
  632. html += '</div>\n'
  633. # 推理过程
  634. tuili_guocheng = element.get('推理过程')
  635. if tuili_guocheng:
  636. html += '<div class="detail-section">\n'
  637. html += '<strong>推理过程:</strong>\n'
  638. html += f'<div class="detail-text">{html_module.escape(tuili_guocheng)}</div>\n'
  639. html += '</div>\n'
  640. else:
  641. if source:
  642. html += '<div class="detail-section">\n'
  643. html += '<strong>来源:</strong>\n'
  644. html += '<div class="detail-content">\n'
  645. for src in source:
  646. html += f'<span class="detail-tag source-tag">{html_module.escape(str(src))}</span>\n'
  647. html += '</div>\n'
  648. html += '</div>\n'
  649. # 上下文验证(适用于具象概念)
  650. context_verification = element.get('上下文验证')
  651. if context_verification:
  652. html += '<div class="detail-section">\n'
  653. html += '<strong>上下文验证:</strong>\n'
  654. html += '<div class="context-verification" style="margin-top: 8px; padding: 12px; background-color: #f8f9fa; border-radius: 4px; border-left: 3px solid #6c757d;">\n'
  655. # 原文位置
  656. original_position = context_verification.get('原文位置', '')
  657. if original_position:
  658. html += '<div class="context-item" style="margin-bottom: 8px;">\n'
  659. html += '<strong style="color: #495057; font-size: 13px;">原文位置:</strong>\n'
  660. 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'
  661. html += '</div>\n'
  662. # 语法成分
  663. grammar_component = context_verification.get('语法成分', '')
  664. if grammar_component:
  665. html += '<div class="context-item" style="margin-bottom: 8px;">\n'
  666. html += '<strong style="color: #495057; font-size: 13px;">语法成分:</strong>\n'
  667. 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'
  668. html += '</div>\n'
  669. # 语境判断
  670. context_judgment = context_verification.get('语境判断', '')
  671. if context_judgment:
  672. html += '<div class="context-item" style="margin-bottom: 0;">\n'
  673. html += '<strong style="color: #495057; font-size: 13px;">语境判断:</strong>\n'
  674. 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'
  675. html += '</div>\n'
  676. html += '</div>\n'
  677. html += '</div>\n'
  678. # 出现段落
  679. if paragraphs_list:
  680. html += '<div class="detail-section">\n'
  681. html += '<strong>出现段落:</strong>\n'
  682. html += '<div class="paragraphs-detail-list">\n'
  683. for para in paragraphs_list:
  684. if isinstance(para, dict):
  685. # 新结构:对象包含段落ID和如何体现
  686. para_id = para.get('段落ID', '')
  687. how = para.get('如何体现', '')
  688. html += '<div class="paragraph-detail-item">\n'
  689. html += f'<span class="detail-tag para-id-tag">{html_module.escape(para_id)}</span>\n'
  690. if how:
  691. html += f'<div class="para-how">{html_module.escape(how)}</div>\n'
  692. html += '</div>\n'
  693. else:
  694. # 旧结构:字符串
  695. html += f'<span class="detail-tag">{html_module.escape(str(para))}</span>\n'
  696. html += '</div>\n'
  697. html += '</div>\n'
  698. # 意图支撑
  699. if intent_support:
  700. html += '<div class="detail-section">\n'
  701. html += '<strong>意图支撑:</strong>\n'
  702. for point_type in ['灵感点', '目的点', '关键点']:
  703. if point_type in intent_support and intent_support[point_type]:
  704. html += f'<div class="score-type">{point_type}</div>\n'
  705. html += '<div class="score-list">\n'
  706. for item in intent_support[point_type]:
  707. point = item.get('点', '')
  708. point_intention = item.get('点的意图', '')
  709. support_reason = item.get('支撑理由', '')
  710. html += '<div class="score-item">\n'
  711. html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
  712. # 显示点的意图
  713. if point_intention:
  714. html += '<div class="point-intention">\n'
  715. html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
  716. html += '</div>\n'
  717. # 显示支撑理由
  718. if support_reason:
  719. html += '<div class="score-reasons">\n'
  720. html += f'<strong style="color: #666;">支撑理由:</strong>\n'
  721. html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
  722. html += '</div>\n'
  723. html += '</div>\n'
  724. html += '</div>\n'
  725. html += '</div>\n'
  726. # 权重明细(放在展开内容最底部,简化为紧凑样式)
  727. if weight_info.get('raw_total', 0) > 0:
  728. wd = weight_info['details']
  729. ss = weight_info['support_stats']
  730. # 判断是形式元素还是实质元素
  731. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  732. html += '<div class="detail-section weight-detail-section" style="margin-top: 12px; border-top: 1px dashed #e0e0e0; padding-top: 10px;">\n'
  733. html += '<strong style="font-size: 13px; color: #555;">权重得分明细:</strong>\n'
  734. html += '<div class="weight-summary-header" style="margin-top: 4px; font-size: 12px; color: #666;">\n'
  735. if is_form:
  736. html += f'权重分:{weight_info["weight_score"]:.1f}(形式元素使用新的权重计算逻辑)\n'
  737. else:
  738. html += f'原始总分:{weight_info["raw_total"]:.1f},权重分:{weight_info["weight_score"]:.1f}(min(100, 原始总分 × 100 / 110))\n'
  739. html += '</div>\n'
  740. html += '<div class="weight-summary-grid" style="margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px;">\n'
  741. # 共性维度得分
  742. if is_form:
  743. # 形式元素:显示频次分、覆盖段落数分、覆盖率分
  744. 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'
  745. if "覆盖段落数分" in wd:
  746. 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'
  747. 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'
  748. else:
  749. # 实质元素:显示频次分、覆盖率分
  750. 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'
  751. 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'
  752. # 支撑维度得分
  753. inspiration_count = ss.get('灵感点数量', 0)
  754. if is_form:
  755. 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'
  756. else:
  757. 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'
  758. purpose_count = ss.get('目的点数量', 0)
  759. if is_form:
  760. 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'
  761. else:
  762. 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'
  763. keypoint_count = ss.get('关键点数量', 0)
  764. if is_form:
  765. 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'
  766. else:
  767. 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'
  768. html += '</div>\n'
  769. html += '</div>\n'
  770. html += '</div>\n'
  771. html += '</li>\n'
  772. return html
  773. def render_element_modal(element: Dict[str, Any]) -> str:
  774. """渲染元素详情的弹窗内容
  775. Args:
  776. element: 元素数据
  777. """
  778. elem_id = element.get('id', '')
  779. name = element.get('名称') or ''
  780. description = element.get('描述') or ''
  781. # 获取类型和维度
  782. dimension = element.get('维度', {})
  783. if isinstance(dimension, dict):
  784. elem_type = dimension.get('一级', '')
  785. elem_type_level2 = dimension.get('二级', '')
  786. else:
  787. elem_type = element.get('类型', '')
  788. elem_type_level2 = ''
  789. # 获取分类
  790. category_data = element.get('分类', '')
  791. category_def = element.get('分类定义', '')
  792. # 获取共性分析
  793. commonality = element.get('共性分析') or {}
  794. coverage = commonality.get('段落覆盖率', 0.0)
  795. frequency = commonality.get('出现频次', 0)
  796. paragraphs_list = commonality.get('出现段落列表', [])
  797. source = element.get('来源', [])
  798. intent_support = get_intent_support_data(element)
  799. weight_info = compute_weight_scores(element)
  800. html = f'<div class="element-modal-content" data-elem-id="{elem_id}">\n'
  801. html += '<div class="modal-header">\n'
  802. if elem_id:
  803. html += f'<span class="modal-id">#{elem_id}</span>\n'
  804. html += f'<h3 class="modal-title">{html_module.escape(name)}</h3>\n'
  805. html += '<button class="modal-close" onclick="closeElementModal()">&times;</button>\n'
  806. html += '</div>\n'
  807. html += '<div class="modal-body">\n'
  808. # 描述
  809. if description:
  810. html += '<div class="detail-section">\n'
  811. html += '<strong>描述:</strong>\n'
  812. html += f'<div class="detail-text">{html_module.escape(description)}</div>\n'
  813. html += '</div>\n'
  814. # 分类定义
  815. if category_def:
  816. html += '<div class="detail-section">\n'
  817. html += '<strong>分类定义:</strong>\n'
  818. html += f'<div class="detail-text">{html_module.escape(category_def)}</div>\n'
  819. html += '</div>\n'
  820. # 针对"形式"维度,显示"支撑"、"推理"和"支撑关系"
  821. if isinstance(dimension, dict) and dimension.get('一级') == '形式':
  822. # 支撑
  823. zhicheng = element.get('支撑')
  824. if zhicheng:
  825. html += '<div class="detail-section">\n'
  826. html += '<strong>支撑:</strong>\n'
  827. html += '<div class="detail-content">\n'
  828. if isinstance(zhicheng, list):
  829. for item in zhicheng:
  830. if isinstance(item, dict):
  831. item_id = item.get('id', '')
  832. item_name = item.get('名称', '')
  833. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  834. else:
  835. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  836. elif isinstance(zhicheng, dict):
  837. for key, values in zhicheng.items():
  838. if isinstance(values, list):
  839. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  840. for item in values:
  841. if isinstance(item, dict):
  842. item_id = item.get('id', '')
  843. item_name = item.get('名称', '')
  844. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  845. else:
  846. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  847. else:
  848. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  849. else:
  850. html += f'<span class="detail-tag">{html_module.escape(str(zhicheng))}</span>\n'
  851. html += '</div>\n'
  852. html += '</div>\n'
  853. # 推理
  854. tuili = element.get('推理')
  855. if tuili:
  856. html += '<div class="detail-section">\n'
  857. html += '<strong>推理:</strong>\n'
  858. html += f'<div class="detail-text">{html_module.escape(tuili)}</div>\n'
  859. html += '</div>\n'
  860. # 支撑关系
  861. intent_support_data = get_intent_support_data(element)
  862. if intent_support_data:
  863. html += '<div class="detail-section">\n'
  864. html += '<strong>支撑关系:</strong>\n'
  865. for point_type in ['灵感点', '目的点', '关键点']:
  866. support_points = intent_support_data.get(point_type) or []
  867. if not support_points:
  868. continue
  869. html += f'<div class="score-type">{point_type}</div>\n'
  870. html += '<div class="score-list">\n'
  871. for support_point in support_points:
  872. if not isinstance(support_point, dict):
  873. continue
  874. point = support_point.get('点', '')
  875. point_intention = support_point.get('点的意图', '')
  876. support_reason = support_point.get('支撑理由', '')
  877. html += '<div class="score-item">\n'
  878. html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
  879. if point_intention:
  880. html += '<div class="point-intention">\n'
  881. html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
  882. html += '</div>\n'
  883. if support_reason:
  884. html += '<div class="score-reasons">\n'
  885. html += f'<strong style="color: #666;">支撑理由:</strong>\n'
  886. html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
  887. html += '</div>\n'
  888. html += '</div>\n'
  889. html += '</div>\n'
  890. html += '</div>\n'
  891. # 针对"隐含概念"
  892. elem_type = element.get('类型', '')
  893. if elem_type == '隐含概念':
  894. laiyuan = element.get('来源')
  895. if laiyuan and isinstance(laiyuan, dict):
  896. html += '<div class="detail-section">\n'
  897. html += '<strong>来源:</strong>\n'
  898. html += '<div class="detail-content">\n'
  899. for key, values in laiyuan.items():
  900. if isinstance(values, list) and values:
  901. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  902. for item in values:
  903. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  904. elif values:
  905. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  906. html += '</div>\n'
  907. html += '</div>\n'
  908. time_range = element.get('时间范围')
  909. if time_range:
  910. html += '<div class="detail-section">\n'
  911. html += '<strong>时间范围:</strong>\n'
  912. html += '<div class="detail-content">\n'
  913. if isinstance(time_range, list):
  914. for tr in time_range:
  915. html += f'<span class="detail-tag source-tag">{html_module.escape(str(tr))}</span>\n'
  916. else:
  917. html += f'<span class="detail-tag source-tag">{html_module.escape(str(time_range))}</span>\n'
  918. html += '</div>\n'
  919. html += '</div>\n'
  920. # 针对"抽象概念"
  921. elif isinstance(dimension, dict) and dimension.get('二级') == '抽象概念':
  922. leixing = element.get('类型')
  923. if leixing:
  924. html += '<div class="detail-section">\n'
  925. html += '<strong>类型:</strong>\n'
  926. html += f'<div class="detail-text">{html_module.escape(str(leixing))}</div>\n'
  927. html += '</div>\n'
  928. laiyuan = element.get('来源')
  929. if laiyuan and isinstance(laiyuan, dict):
  930. html += '<div class="detail-section">\n'
  931. html += '<strong>来源:</strong>\n'
  932. html += '<div class="detail-content">\n'
  933. for key, values in laiyuan.items():
  934. if isinstance(values, list):
  935. html += f'<div class="detail-tag category-level1">{html_module.escape(key)}</div>\n'
  936. for item in values:
  937. if isinstance(item, dict):
  938. item_id = item.get('id', '')
  939. item_name = item.get('名称', '')
  940. html += f'<span class="detail-tag">{html_module.escape(f"{item_id}: {item_name}")}</span>\n'
  941. else:
  942. html += f'<span class="detail-tag">{html_module.escape(str(item))}</span>\n'
  943. else:
  944. html += f'<span class="detail-tag">{html_module.escape(str(values))}</span>\n'
  945. html += '</div>\n'
  946. html += '</div>\n'
  947. tuili_guocheng = element.get('推理过程')
  948. if tuili_guocheng:
  949. html += '<div class="detail-section">\n'
  950. html += '<strong>推理过程:</strong>\n'
  951. html += f'<div class="detail-text">{html_module.escape(tuili_guocheng)}</div>\n'
  952. html += '</div>\n'
  953. else:
  954. if source:
  955. html += '<div class="detail-section">\n'
  956. html += '<strong>来源:</strong>\n'
  957. html += '<div class="detail-content">\n'
  958. for src in source:
  959. html += f'<span class="detail-tag source-tag">{html_module.escape(str(src))}</span>\n'
  960. html += '</div>\n'
  961. html += '</div>\n'
  962. # 上下文验证
  963. context_verification = element.get('上下文验证')
  964. if context_verification:
  965. html += '<div class="detail-section">\n'
  966. html += '<strong>上下文验证:</strong>\n'
  967. html += '<div class="context-verification">\n'
  968. original_position = context_verification.get('原文位置', '')
  969. if original_position:
  970. html += '<div class="context-item">\n'
  971. html += '<strong>原文位置:</strong>\n'
  972. html += f'<div class="context-text">{html_module.escape(original_position)}</div>\n'
  973. html += '</div>\n'
  974. grammar_component = context_verification.get('语法成分', '')
  975. if grammar_component:
  976. html += '<div class="context-item">\n'
  977. html += '<strong>语法成分:</strong>\n'
  978. html += f'<span class="grammar-tag">{html_module.escape(grammar_component)}</span>\n'
  979. html += '</div>\n'
  980. context_judgment = context_verification.get('语境判断', '')
  981. if context_judgment:
  982. html += '<div class="context-item">\n'
  983. html += '<strong>语境判断:</strong>\n'
  984. html += f'<div class="context-text">{html_module.escape(context_judgment)}</div>\n'
  985. html += '</div>\n'
  986. html += '</div>\n'
  987. html += '</div>\n'
  988. # 出现段落
  989. if paragraphs_list:
  990. html += '<div class="detail-section">\n'
  991. html += '<strong>出现段落:</strong>\n'
  992. html += '<div class="paragraphs-detail-list">\n'
  993. for para in paragraphs_list:
  994. if isinstance(para, dict):
  995. para_id = para.get('段落ID', '')
  996. how = para.get('如何体现', '')
  997. html += '<div class="paragraph-detail-item">\n'
  998. html += f'<span class="detail-tag para-id-tag">{html_module.escape(para_id)}</span>\n'
  999. if how:
  1000. html += f'<div class="para-how">{html_module.escape(how)}</div>\n'
  1001. html += '</div>\n'
  1002. else:
  1003. html += f'<span class="detail-tag">{html_module.escape(str(para))}</span>\n'
  1004. html += '</div>\n'
  1005. html += '</div>\n'
  1006. # 意图支撑
  1007. if intent_support:
  1008. html += '<div class="detail-section">\n'
  1009. html += '<strong>意图支撑:</strong>\n'
  1010. for point_type in ['灵感点', '目的点', '关键点']:
  1011. if point_type in intent_support and intent_support[point_type]:
  1012. html += f'<div class="score-type">{point_type}</div>\n'
  1013. html += '<div class="score-list">\n'
  1014. for item in intent_support[point_type]:
  1015. point = item.get('点', '')
  1016. point_intention = item.get('点的意图', '')
  1017. support_reason = item.get('支撑理由', '')
  1018. html += '<div class="score-item">\n'
  1019. html += f'<div class="score-point">{html_module.escape(point)}</div>\n'
  1020. if point_intention:
  1021. html += '<div class="point-intention">\n'
  1022. html += f'<strong style="color: #666;">点的意图:</strong>{html_module.escape(point_intention)}\n'
  1023. html += '</div>\n'
  1024. if support_reason:
  1025. html += '<div class="score-reasons">\n'
  1026. html += f'<strong style="color: #666;">支撑理由:</strong>\n'
  1027. html += f'<div class="score-reason">{html_module.escape(support_reason)}</div>\n'
  1028. html += '</div>\n'
  1029. html += '</div>\n'
  1030. html += '</div>\n'
  1031. html += '</div>\n'
  1032. # 权重明细
  1033. if weight_info.get('raw_total', 0) > 0:
  1034. wd = weight_info['details']
  1035. ss = weight_info['support_stats']
  1036. is_form = isinstance(dimension, dict) and dimension.get('一级') == '形式'
  1037. html += '<div class="detail-section weight-detail-section">\n'
  1038. html += '<strong>权重得分明细:</strong>\n'
  1039. html += '<div class="weight-summary-header">\n'
  1040. if is_form:
  1041. html += f'权重分:{weight_info["weight_score"]:.1f}(形式元素使用新的权重计算逻辑)\n'
  1042. else:
  1043. html += f'原始总分:{weight_info["raw_total"]:.1f},权重分:{weight_info["weight_score"]:.1f}(min(100, 原始总分 × 100 / 110))\n'
  1044. html += '</div>\n'
  1045. html += '<div class="weight-summary-grid">\n'
  1046. if is_form:
  1047. html += f'<span class="weight-chip">频次分 {wd.get("频次分", 0):.1f}</span>\n'
  1048. if "覆盖段落数分" in wd:
  1049. html += f'<span class="weight-chip">覆盖段落数分 {wd.get("覆盖段落数分", 0):.1f}</span>\n'
  1050. html += f'<span class="weight-chip">覆盖率分 {wd.get("覆盖率分", 0):.1f}</span>\n'
  1051. else:
  1052. html += f'<span class="weight-chip">频次分 {wd.get("频次分", 0):.1f} / 30</span>\n'
  1053. html += f'<span class="weight-chip">覆盖率分 {wd.get("覆盖率分", 0):.1f} / 30</span>\n'
  1054. inspiration_count = ss.get('灵感点数量', 0)
  1055. if is_form:
  1056. html += f'<span class="weight-chip">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f}({inspiration_count} 个)</span>\n'
  1057. else:
  1058. html += f'<span class="weight-chip">灵感点支撑分 {wd.get("灵感点支撑分", 0):.1f} / 25({inspiration_count} 个)</span>\n'
  1059. purpose_count = ss.get('目的点数量', 0)
  1060. if is_form:
  1061. html += f'<span class="weight-chip">目的点支撑分 {wd.get("目的点支撑分", 0):.1f}({purpose_count} 个)</span>\n'
  1062. else:
  1063. html += f'<span class="weight-chip">目的点支撑分 {wd.get("目的点支撑分", 0):.1f} / 15({purpose_count} 个)</span>\n'
  1064. keypoint_count = ss.get('关键点数量', 0)
  1065. if is_form:
  1066. html += f'<span class="weight-chip">关键点支撑分 {wd.get("关键点支撑分", 0):.1f}({keypoint_count} 个)</span>\n'
  1067. else:
  1068. html += f'<span class="weight-chip">关键点支撑分 {wd.get("关键点支撑分", 0):.1f} / 10({keypoint_count} 个)</span>\n'
  1069. html += '</div>\n'
  1070. html += '</div>\n'
  1071. html += '</div>\n' # modal-body
  1072. html += '</div>\n' # element-modal-content
  1073. return html
  1074. def generate_tab3_content(data: Dict[str, Any]) -> str:
  1075. """生成Tab3内容:按层次展示(实质/形式 → 具体元素/具体概念/抽象概念 → 树形展示)"""
  1076. html = '<div class="tab-content" id="tab3" style="display:none;">\n'
  1077. # 添加全局控制按钮(移除展开/收起按钮)
  1078. html += '<div class="global-controls">\n'
  1079. html += ' <div class="color-legend">\n'
  1080. html += ' <span class="legend-title">颜色图例(主导因素):</span>\n'
  1081. html += ' <span class="legend-item legend-coverage">覆盖率</span>\n'
  1082. html += ' <span class="legend-item legend-frequency">频次</span>\n'
  1083. html += ' <span class="legend-item legend-intent">意图支撑</span>\n'
  1084. html += ' </div>\n'
  1085. html += '</div>\n'
  1086. # 收集所有元素用于弹窗
  1087. all_elements_dict = {}
  1088. if '脚本理解' in data:
  1089. script = data['脚本理解']
  1090. # 尝试获取元素列表,如果不存在则合并实质列表和形式列表
  1091. elements = script.get('元素列表', [])
  1092. if not elements:
  1093. substance_list = script.get('实质列表', [])
  1094. form_list = script.get('形式列表', [])
  1095. elements = substance_list + form_list
  1096. # 收集所有元素到字典中(用于弹窗)
  1097. for elem in elements:
  1098. elem_id = elem.get('id', '')
  1099. if elem_id:
  1100. all_elements_dict[elem_id] = elem
  1101. # 第一层:按维度.一级分组(实质 vs 形式)
  1102. level1_groups = {}
  1103. for elem in elements:
  1104. dimension = elem.get('维度', {})
  1105. if isinstance(dimension, dict):
  1106. level1 = dimension.get('一级', '实质')
  1107. else:
  1108. # 兼容旧结构
  1109. level1 = elem.get('类型', '实质')
  1110. if level1 not in level1_groups:
  1111. level1_groups[level1] = []
  1112. level1_groups[level1].append(elem)
  1113. # 按顺序渲染:实质、形式
  1114. for level1_name in ['实质', '形式']:
  1115. if level1_name not in level1_groups:
  1116. continue
  1117. level1_elements = level1_groups[level1_name]
  1118. html += '<div class="section level1-section">\n'
  1119. html += f'<div class="level1-header collapsible" onclick="toggleLevel1(this)">\n'
  1120. html += '<span class="level-toggle-icon">▼</span>\n'
  1121. html += f'<h2 class="level1-title">{level1_name} ({len(level1_elements)}个)</h2>\n'
  1122. html += '</div>\n'
  1123. html += '<div class="level1-content">\n'
  1124. # 第二层:按维度.二级分组
  1125. level2_groups = {}
  1126. for elem in level1_elements:
  1127. dimension = elem.get('维度', {})
  1128. elem_type = elem.get('类型', '')
  1129. # 隐含概念:优先通过类型判断(因为维度二级可能是"隐含概念"或"抽象概念")
  1130. if elem_type == '隐含概念':
  1131. level2 = '隐含概念'
  1132. elif isinstance(dimension, dict):
  1133. level2 = dimension.get('二级', '具体元素')
  1134. else:
  1135. # 兼容旧结构
  1136. elem_type_old = elem.get('类型', '实质')
  1137. if elem_type_old == '实质':
  1138. level2 = '具体元素'
  1139. elif elem_type_old == '具象概念':
  1140. level2 = '具体概念'
  1141. else:
  1142. level2 = '抽象概念'
  1143. if level2 not in level2_groups:
  1144. level2_groups[level2] = []
  1145. level2_groups[level2].append(elem)
  1146. # 根据一级维度确定二级维度遍历顺序
  1147. if level1_name == '实质':
  1148. level2_order = ['具体元素', '具象概念', '隐含概念', '抽象概念']
  1149. else: # 形式
  1150. level2_order = ['具体元素形式', '具象概念形式', '整体形式']
  1151. # 按顺序渲染二级维度
  1152. for level2_name in level2_order:
  1153. if level2_name not in level2_groups:
  1154. continue
  1155. level2_elements = level2_groups[level2_name]
  1156. html += '<div class="level2-section">\n'
  1157. html += f'<div class="level2-header collapsible" onclick="toggleLevel2(this)">\n'
  1158. html += '<span class="level-toggle-icon">▼</span>\n'
  1159. html += f'<h3 class="level2-title">{level2_name} ({len(level2_elements)}个)</h3>\n'
  1160. html += '</div>\n'
  1161. html += '<div class="level2-content">\n'
  1162. # 第三层:按树形分类结构组织
  1163. hierarchical_categories = group_elements_by_hierarchical_category(level2_elements)
  1164. # 渲染树形分类结构
  1165. for cat_level1_name, cat_level1_data in hierarchical_categories.items():
  1166. # 收集该一级分类下的所有元素
  1167. all_cat_elements = cat_level1_data['elements'][:]
  1168. for level2_elems in cat_level1_data['level2_groups'].values():
  1169. all_cat_elements.extend(level2_elems)
  1170. if not all_cat_elements:
  1171. continue
  1172. # 计算一级分类的统计信息
  1173. avg_coverage = sum(get_element_coverage(e) for e in all_cat_elements) / len(all_cat_elements)
  1174. avg_intent_count = sum(calculate_intent_support_count(e) for e in all_cat_elements) / len(all_cat_elements)
  1175. html += '<div class="category-group collapsible">\n'
  1176. html += '<div class="category-header" onclick="toggleCategoryGroup(this)">\n'
  1177. html += '<span class="category-toggle-icon">▼</span>\n'
  1178. html += f'<h4 class="category-title">{html_module.escape(cat_level1_name)} ({len(all_cat_elements)}个)</h4>\n'
  1179. html += '<div class="category-stats">\n'
  1180. html += f'<span class="stat-badge">平均覆盖率: {avg_coverage:.2%}</span>\n'
  1181. html += f'<span class="stat-badge">平均意图支撑: {avg_intent_count:.1f}</span>\n'
  1182. html += '</div>\n'
  1183. html += '</div>\n'
  1184. html += '<div class="category-content">\n'
  1185. # 渲染一级分类直接包含的元素
  1186. if cat_level1_data['elements']:
  1187. html += '<ul class="element-list">\n'
  1188. for elem in cat_level1_data['elements']:
  1189. html += render_element_item(elem, all_cat_elements)
  1190. html += '</ul>\n'
  1191. # 渲染二级分类
  1192. for cat_level2_name, cat_level2_elements in cat_level1_data['level2_groups'].items():
  1193. if not cat_level2_elements:
  1194. continue
  1195. # 计算二级分类的统计信息
  1196. avg_coverage_l2 = sum(get_element_coverage(e) for e in cat_level2_elements) / len(cat_level2_elements)
  1197. avg_intent_count_l2 = sum(calculate_intent_support_count(e) for e in cat_level2_elements) / len(cat_level2_elements)
  1198. html += '<div class="subcategory-group collapsible">\n'
  1199. html += '<div class="subcategory-header" onclick="toggleSubcategoryGroup(this)">\n'
  1200. html += '<span class="subcategory-toggle-icon">▼</span>\n'
  1201. html += f'<h5 class="subcategory-title">{html_module.escape(cat_level2_name)} ({len(cat_level2_elements)}个)</h5>\n'
  1202. html += '<div class="subcategory-stats">\n'
  1203. html += f'<span class="stat-badge-small">覆盖率: {avg_coverage_l2:.2%}</span>\n'
  1204. html += '</div>\n'
  1205. html += '</div>\n'
  1206. html += '<div class="subcategory-content">\n'
  1207. html += '<ul class="element-list">\n'
  1208. for elem in cat_level2_elements:
  1209. html += render_element_item(elem, cat_level2_elements)
  1210. html += '</ul>\n'
  1211. html += '</div>\n'
  1212. html += '</div>\n'
  1213. html += '</div>\n'
  1214. html += '</div>\n'
  1215. html += '</div>\n' # level2-content
  1216. html += '</div>\n' # level2-section
  1217. html += '</div>\n' # level1-content
  1218. html += '</div>\n' # level1-section
  1219. # 预生成所有元素的弹窗内容(隐藏)
  1220. html += '<div id="elementModalTemplates" style="display:none;">\n'
  1221. for elem_id, elem in all_elements_dict.items():
  1222. html += render_element_modal(elem)
  1223. html += '</div>\n'
  1224. # 添加弹窗结构
  1225. html += '<div id="elementModal" class="element-modal" style="display:none;">\n'
  1226. html += '<div class="element-modal-overlay" onclick="closeElementModal()"></div>\n'
  1227. html += '<div class="element-modal-dialog">\n'
  1228. html += '<div id="elementModalContent" class="element-modal-content-wrapper">\n'
  1229. html += '<!-- 弹窗内容将通过JavaScript动态填充 -->\n'
  1230. html += '</div>\n'
  1231. html += '</div>\n'
  1232. html += '</div>\n'
  1233. # 添加CSS样式
  1234. html += '<style>\n'
  1235. html += '''
  1236. /* 元素卡片样式 */
  1237. .element-list {
  1238. display: grid;
  1239. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  1240. gap: 20px;
  1241. list-style: none;
  1242. padding: 0;
  1243. margin: 20px 0;
  1244. }
  1245. .element-card {
  1246. background: white;
  1247. border-radius: 8px;
  1248. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1249. transition: all 0.3s ease;
  1250. overflow: hidden;
  1251. }
  1252. .element-card:hover {
  1253. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  1254. transform: translateY(-2px);
  1255. }
  1256. .element-card.dominant-coverage {
  1257. border-left: 4px solid #667eea;
  1258. }
  1259. .element-card.dominant-frequency {
  1260. border-left: 4px solid #f093fb;
  1261. }
  1262. .element-card.dominant-intent_support {
  1263. border-left: 4px solid #4facfe;
  1264. }
  1265. .element-card-body {
  1266. padding: 16px;
  1267. }
  1268. .element-card-header {
  1269. display: flex;
  1270. align-items: center;
  1271. gap: 8px;
  1272. margin-bottom: 12px;
  1273. }
  1274. .element-card-id {
  1275. color: #667eea;
  1276. font-weight: 600;
  1277. font-size: 14px;
  1278. }
  1279. .element-card-name {
  1280. margin: 0;
  1281. font-size: 18px;
  1282. font-weight: 600;
  1283. color: #333;
  1284. flex: 1;
  1285. }
  1286. .element-card-stats {
  1287. display: flex;
  1288. flex-wrap: wrap;
  1289. gap: 8px;
  1290. margin-bottom: 12px;
  1291. }
  1292. .stat-badge {
  1293. display: inline-block;
  1294. padding: 4px 10px;
  1295. border-radius: 12px;
  1296. font-size: 12px;
  1297. font-weight: 500;
  1298. background: #f5f5f5;
  1299. color: #666;
  1300. }
  1301. .stat-badge.stat-weight {
  1302. background: #667eea;
  1303. color: white;
  1304. }
  1305. .stat-badge.stat-coverage {
  1306. background: #e8f0ff;
  1307. color: #667eea;
  1308. }
  1309. .stat-badge.stat-frequency {
  1310. background: #fce4ec;
  1311. color: #f093fb;
  1312. }
  1313. .stat-badge.stat-intent {
  1314. background: #e0f7fa;
  1315. color: #4facfe;
  1316. }
  1317. .stat-badge.stat-highlight {
  1318. font-weight: 700;
  1319. transform: scale(1.05);
  1320. }
  1321. .element-card-description {
  1322. color: #666;
  1323. font-size: 14px;
  1324. line-height: 1.6;
  1325. margin-bottom: 12px;
  1326. }
  1327. .element-detail-btn {
  1328. width: 100%;
  1329. padding: 10px;
  1330. background: #667eea;
  1331. color: white;
  1332. border: none;
  1333. border-radius: 6px;
  1334. font-size: 14px;
  1335. font-weight: 500;
  1336. cursor: pointer;
  1337. transition: all 0.2s;
  1338. }
  1339. .element-detail-btn:hover {
  1340. background: #5568d3;
  1341. transform: translateY(-1px);
  1342. box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
  1343. }
  1344. /* 弹窗样式 */
  1345. .element-modal {
  1346. display: none;
  1347. position: fixed;
  1348. top: 0;
  1349. left: 0;
  1350. width: 100%;
  1351. height: 100%;
  1352. z-index: 1000;
  1353. align-items: center;
  1354. justify-content: center;
  1355. }
  1356. .element-modal-overlay {
  1357. position: absolute;
  1358. top: 0;
  1359. left: 0;
  1360. width: 100%;
  1361. height: 100%;
  1362. background: rgba(0, 0, 0, 0.5);
  1363. backdrop-filter: blur(4px);
  1364. }
  1365. .element-modal-dialog {
  1366. position: relative;
  1367. width: 800px;
  1368. height: 90vh;
  1369. background: white;
  1370. border-radius: 12px;
  1371. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  1372. z-index: 1001;
  1373. display: flex;
  1374. flex-direction: column;
  1375. overflow: hidden;
  1376. }
  1377. .element-modal-content-wrapper {
  1378. width: 100%;
  1379. height: 100%;
  1380. display: flex;
  1381. flex-direction: column;
  1382. overflow: hidden;
  1383. }
  1384. .element-modal-content {
  1385. width: 100%;
  1386. height: 100%;
  1387. display: flex;
  1388. flex-direction: column;
  1389. padding: 0;
  1390. overflow: hidden;
  1391. }
  1392. .modal-header {
  1393. width: 100%;
  1394. display: flex;
  1395. align-items: center;
  1396. gap: 12px;
  1397. padding: 20px 24px;
  1398. border-bottom: 1px solid rgba(255, 255, 255, 0.2);
  1399. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  1400. color: white;
  1401. border-radius: 12px 12px 0 0;
  1402. position: sticky;
  1403. top: 0;
  1404. z-index: 10;
  1405. flex-shrink: 0;
  1406. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  1407. }
  1408. .modal-id {
  1409. font-weight: 600;
  1410. font-size: 14px;
  1411. opacity: 0.9;
  1412. }
  1413. .modal-title {
  1414. margin: 0;
  1415. font-size: 20px;
  1416. font-weight: 600;
  1417. flex: 1;
  1418. }
  1419. .modal-close {
  1420. background: rgba(255, 255, 255, 0.2);
  1421. border: none;
  1422. color: white;
  1423. font-size: 24px;
  1424. width: 32px;
  1425. height: 32px;
  1426. border-radius: 50%;
  1427. cursor: pointer;
  1428. display: flex;
  1429. align-items: center;
  1430. justify-content: center;
  1431. transition: all 0.2s;
  1432. }
  1433. .modal-close:hover {
  1434. background: rgba(255, 255, 255, 0.3);
  1435. transform: rotate(90deg);
  1436. }
  1437. .modal-body {
  1438. width: 100%;
  1439. flex: 1;
  1440. padding: 24px;
  1441. overflow-y: auto;
  1442. overflow-x: hidden;
  1443. min-height: 0;
  1444. }
  1445. .detail-section {
  1446. margin-bottom: 20px;
  1447. }
  1448. .detail-section strong {
  1449. display: block;
  1450. color: #667eea;
  1451. font-size: 14px;
  1452. font-weight: 600;
  1453. margin-bottom: 8px;
  1454. }
  1455. .detail-text {
  1456. color: #555;
  1457. line-height: 1.6;
  1458. padding: 12px;
  1459. background: #f8f9fa;
  1460. border-radius: 6px;
  1461. border-left: 3px solid #667eea;
  1462. }
  1463. .detail-content {
  1464. display: flex;
  1465. flex-wrap: wrap;
  1466. gap: 8px;
  1467. margin-top: 8px;
  1468. }
  1469. .detail-tag {
  1470. display: inline-block;
  1471. padding: 6px 12px;
  1472. background: #e8f0ff;
  1473. color: #667eea;
  1474. border-radius: 16px;
  1475. font-size: 13px;
  1476. font-weight: 500;
  1477. }
  1478. .detail-tag.category-level1 {
  1479. width: 100%;
  1480. background: #667eea;
  1481. color: white;
  1482. font-weight: 600;
  1483. margin-top: 8px;
  1484. }
  1485. .detail-tag.source-tag {
  1486. background: #fce4ec;
  1487. color: #f093fb;
  1488. }
  1489. .detail-tag.para-id-tag {
  1490. background: #e0f7fa;
  1491. color: #4facfe;
  1492. }
  1493. .weight-chip {
  1494. display: inline-block;
  1495. padding: 6px 12px;
  1496. background: #f5f5f5;
  1497. color: #333;
  1498. border-radius: 16px;
  1499. font-size: 12px;
  1500. margin: 4px;
  1501. }
  1502. .weight-summary-grid {
  1503. display: flex;
  1504. flex-wrap: wrap;
  1505. gap: 8px;
  1506. margin-top: 12px;
  1507. }
  1508. .weight-summary-header {
  1509. font-size: 13px;
  1510. color: #666;
  1511. margin-top: 8px;
  1512. }
  1513. .weight-detail-section {
  1514. margin-top: 20px;
  1515. padding-top: 20px;
  1516. border-top: 1px dashed #e0e0e0;
  1517. }
  1518. .score-type {
  1519. font-weight: 600;
  1520. color: #667eea;
  1521. margin-top: 16px;
  1522. margin-bottom: 8px;
  1523. font-size: 15px;
  1524. }
  1525. .score-list {
  1526. margin-left: 16px;
  1527. }
  1528. .score-item {
  1529. margin-bottom: 12px;
  1530. padding: 12px;
  1531. background: #f8f9fa;
  1532. border-radius: 6px;
  1533. border-left: 3px solid #667eea;
  1534. }
  1535. .score-point {
  1536. font-weight: 600;
  1537. color: #333;
  1538. margin-bottom: 8px;
  1539. }
  1540. .point-intention, .score-reasons {
  1541. margin-top: 8px;
  1542. font-size: 13px;
  1543. color: #666;
  1544. }
  1545. .score-reason {
  1546. margin-top: 4px;
  1547. padding: 8px;
  1548. background: white;
  1549. border-radius: 4px;
  1550. }
  1551. .context-verification {
  1552. margin-top: 8px;
  1553. padding: 12px;
  1554. background: #f8f9fa;
  1555. border-radius: 4px;
  1556. border-left: 3px solid #6c757d;
  1557. }
  1558. .context-item {
  1559. margin-bottom: 8px;
  1560. }
  1561. .context-text {
  1562. margin-top: 4px;
  1563. padding: 6px 10px;
  1564. background: white;
  1565. border-radius: 3px;
  1566. color: #212529;
  1567. font-style: italic;
  1568. }
  1569. .grammar-tag {
  1570. margin-left: 8px;
  1571. padding: 3px 10px;
  1572. background: #e7f3ff;
  1573. color: #0066cc;
  1574. border-radius: 3px;
  1575. font-size: 12px;
  1576. }
  1577. .paragraphs-detail-list {
  1578. display: flex;
  1579. flex-direction: column;
  1580. gap: 8px;
  1581. margin-top: 8px;
  1582. }
  1583. .paragraph-detail-item {
  1584. display: flex;
  1585. align-items: flex-start;
  1586. gap: 8px;
  1587. }
  1588. .para-how {
  1589. flex: 1;
  1590. padding: 8px;
  1591. background: white;
  1592. border-radius: 4px;
  1593. color: #555;
  1594. font-size: 13px;
  1595. }
  1596. @media (max-width: 768px) {
  1597. .element-list {
  1598. grid-template-columns: 1fr;
  1599. }
  1600. .element-modal-dialog {
  1601. width: 95%;
  1602. height: 95vh;
  1603. }
  1604. .modal-body {
  1605. padding: 16px;
  1606. }
  1607. }
  1608. '''
  1609. html += '</style>\n'
  1610. # 添加JavaScript代码
  1611. html += '<script>\n'
  1612. html += '''
  1613. function openElementModal(elemId) {
  1614. const modal = document.getElementById('elementModal');
  1615. const contentWrapper = document.getElementById('elementModalContent');
  1616. const templates = document.getElementById('elementModalTemplates');
  1617. // 查找预生成的弹窗内容
  1618. const modalContent = templates.querySelector(`.element-modal-content[data-elem-id="${elemId}"]`);
  1619. if (modalContent) {
  1620. contentWrapper.innerHTML = modalContent.outerHTML;
  1621. modal.style.display = 'flex';
  1622. document.body.style.overflow = 'hidden';
  1623. } else {
  1624. console.error('Modal content not found for element:', elemId);
  1625. }
  1626. }
  1627. function closeElementModal() {
  1628. const modal = document.getElementById('elementModal');
  1629. modal.style.display = 'none';
  1630. document.body.style.overflow = '';
  1631. }
  1632. // ESC键关闭弹窗
  1633. document.addEventListener('keydown', function(e) {
  1634. if (e.key === 'Escape') {
  1635. closeElementModal();
  1636. }
  1637. });
  1638. '''
  1639. html += '</script>\n'
  1640. html += '</div>\n'
  1641. return html