tab5.py 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956
  1. #!/usr/bin/env python3
  2. """
  3. Tab5内容生成器 - 实质与形式的双向支撑关系图
  4. 展示:选题点(来自实质) ← 实质点 → 形式点 → 选题点(来自形式)
  5. """
  6. import html as html_module
  7. import json
  8. from typing import Dict, Any, List
  9. def get_intent_support_data(element: Dict[str, Any]) -> Dict[str, Any]:
  10. """
  11. 获取元素的意图支撑数据(兼容新旧数据结构)
  12. 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  13. 这两个字段内部存储的都是意图支撑数据
  14. Args:
  15. element: 元素数据
  16. Returns:
  17. 意图支撑数据字典,格式:{"灵感点": [...], "目的点": [...], "关键点": [...]}
  18. """
  19. # 优先使用"意图支撑"字段
  20. intent_support = element.get('意图支撑')
  21. if intent_support and isinstance(intent_support, dict):
  22. return intent_support
  23. # 如果没有"意图支撑",则使用"多维度评分"字段(兼容旧数据)
  24. multi_scores = element.get('多维度评分')
  25. if multi_scores and isinstance(multi_scores, dict):
  26. return multi_scores
  27. # 都没有则返回空字典
  28. return {}
  29. def generate_tab5_content(data: Dict[str, Any]) -> str:
  30. """生成Tab5内容:实质与形式的双向支撑关系图(4列布局)"""
  31. html = '<div class="tab-content" id="tab5">\n'
  32. # 提取数据
  33. script_data = data.get('脚本理解', {})
  34. form_list = script_data.get('形式列表', [])
  35. substance_list = script_data.get('实质列表', [])
  36. # 处理灵感点:可能是列表,也可能在对象中
  37. inspiration_data = data.get('灵感点', [])
  38. if isinstance(inspiration_data, list):
  39. inspiration_points = inspiration_data
  40. elif isinstance(inspiration_data, dict):
  41. inspiration_points = inspiration_data.get('inspiration_points', [])
  42. else:
  43. inspiration_points = []
  44. # 处理目的点:可能是列表,也可能在对象的purposes字段中
  45. purpose_data = data.get('目的点', [])
  46. if isinstance(purpose_data, list):
  47. purpose_points = purpose_data
  48. elif isinstance(purpose_data, dict):
  49. purpose_points = purpose_data.get('purposes', [])
  50. else:
  51. purpose_points = []
  52. # 处理关键点:可能是列表,也可能在对象的key_points字段中
  53. keypoint_data = data.get('关键点', [])
  54. if isinstance(keypoint_data, list):
  55. key_points = keypoint_data
  56. elif isinstance(keypoint_data, dict):
  57. key_points = keypoint_data.get('key_points', [])
  58. else:
  59. key_points = []
  60. if not substance_list and not form_list:
  61. html += '<div class="empty-state">暂无实质点和形式点数据</div>\n'
  62. html += '</div>\n'
  63. return html
  64. # 分类实质点
  65. concrete_elements = []
  66. concrete_concepts = []
  67. implicit_concepts = []
  68. abstract_concepts = []
  69. for substance in substance_list:
  70. dimension_2 = substance.get('维度', {}).get('二级', '')
  71. elem_type = substance.get('类型', '')
  72. # 隐含概念:优先通过类型判断(因为维度二级可能是"隐含概念"或"抽象概念")
  73. if elem_type == '隐含概念':
  74. implicit_concepts.append(substance)
  75. elif dimension_2 == '具体元素':
  76. concrete_elements.append(substance)
  77. elif dimension_2 == '具象概念': # 修改:从"具体概念"改为"具象概念"
  78. concrete_concepts.append(substance)
  79. elif dimension_2 == '抽象概念':
  80. abstract_concepts.append(substance)
  81. # 分类形式点
  82. concrete_element_forms = []
  83. concrete_concept_forms = []
  84. overall_forms = []
  85. for form in form_list:
  86. dimension_2 = form.get('维度', {}).get('二级', '')
  87. if dimension_2 == '具体元素形式':
  88. concrete_element_forms.append(form)
  89. elif dimension_2 == '具象概念形式': # 修改:从"具体概念形式"改为"具象概念形式"
  90. concrete_concept_forms.append(form)
  91. elif dimension_2 == '整体形式':
  92. overall_forms.append(form)
  93. # 构建关系数据
  94. relationships = build_bidirectional_relationships(
  95. concrete_elements, concrete_concepts, implicit_concepts, abstract_concepts,
  96. concrete_element_forms, concrete_concept_forms, overall_forms,
  97. inspiration_points, purpose_points, key_points
  98. )
  99. # 添加标题和说明
  100. html += '<div class="tab5-header">\n'
  101. html += '<h2 class="tab5-title">实质与形式的双向支撑关系图</h2>\n'
  102. html += '<div class="tab5-description">\n'
  103. html += '<p>4列展示:<strong>选题点(实质支撑)</strong> ← <strong>实质点</strong> | <strong>形式点</strong> → <strong>选题点(形式支撑)</strong></p>\n'
  104. html += '<ul class="relationship-rules">\n'
  105. html += '<li>左侧选题点:显示实质点对灵感点、关键点、目的点的支撑关系</li>\n'
  106. html += '<li>右侧选题点:显示形式点对灵感点、关键点、目的点的支撑关系</li>\n'
  107. html += '</ul>\n'
  108. html += '</div>\n'
  109. html += '</div>\n'
  110. # SVG 连线容器(放在4列布局之前,作为背景层)
  111. html += '<div class="tab5-svg-container">\n'
  112. html += '<svg id="tab5-connection-svg" width="100%" height="100%">\n'
  113. html += '<!-- 连线将在这里绘制 -->\n'
  114. html += '</svg>\n'
  115. html += '</div>\n'
  116. # 主要内容区域(4列布局)
  117. html += '<div class="tab5-four-column-container">\n'
  118. # 第1列:左侧选题点(来自实质点的支撑关系)
  119. html += '<div class="tab5-column tab5-left-targets">\n'
  120. html += '<h3 class="panel-title">选题点<br/><span style="font-size:0.8em;font-weight:normal;color:#6c757d;">(实质支撑)</span></h3>\n'
  121. # 灵感点(左)
  122. if inspiration_points:
  123. html += '<div class="target-group">\n'
  124. html += '<h4 class="group-title">灵感点</h4>\n'
  125. html += '<div class="target-items">\n'
  126. for idx, point in enumerate(inspiration_points, 1):
  127. # 确保 point 是字典
  128. if not isinstance(point, dict):
  129. continue
  130. # 使用提取的特征中的特征名称,每个特征名称一个卡片
  131. features = point.get('提取的特征', [])
  132. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  133. if feature_names:
  134. # 为每个特征名称创建一个独立ID的卡片
  135. for feature_idx, feature_name in enumerate(feature_names, 1):
  136. feature_id = f'inspiration-{idx}-{feature_idx}'
  137. html += f'<div class="target-card inspiration-card" data-type="inspiration-substance" data-id="{feature_id}" onclick="selectLeftTarget(' + f"'inspiration', '{idx}-{feature_idx}'" + ')">\n'
  138. html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
  139. html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
  140. html += '</div>\n'
  141. else:
  142. # 如果没有特征,使用原始的灵感点文本
  143. display_text = point.get('灵感点', '')
  144. html += f'<div class="target-card inspiration-card" data-type="inspiration-substance" data-id="inspiration-{idx}" onclick="selectLeftTarget(' + f"'inspiration', {idx}" + ')">\n'
  145. html += f'<div class="card-number">#{idx}</div>\n'
  146. html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
  147. html += '</div>\n'
  148. html += '</div>\n'
  149. html += '</div>\n'
  150. # 关键点(左)
  151. if key_points:
  152. html += '<div class="target-group">\n'
  153. html += '<h4 class="group-title">关键点</h4>\n'
  154. html += '<div class="target-items">\n'
  155. for idx, point in enumerate(key_points, 1):
  156. # 确保 point 是字典
  157. if not isinstance(point, dict):
  158. continue
  159. # 使用提取的特征中的特征名称,每个特征名称一个卡片
  160. features = point.get('提取的特征', [])
  161. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  162. if feature_names:
  163. # 为每个特征名称创建一个独立ID的卡片
  164. for feature_idx, feature_name in enumerate(feature_names, 1):
  165. feature_id = f'keypoint-{idx}-{feature_idx}'
  166. html += f'<div class="target-card keypoint-card" data-type="keypoint-substance" data-id="{feature_id}" onclick="selectLeftTarget(' + f"'keypoint', '{idx}-{feature_idx}'" + ')">\n'
  167. html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
  168. html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
  169. html += '</div>\n'
  170. else:
  171. # 如果没有特征,使用原始的关键点文本
  172. display_text = point.get('关键点', '')
  173. html += f'<div class="target-card keypoint-card" data-type="keypoint-substance" data-id="keypoint-{idx}" onclick="selectLeftTarget(' + f"'keypoint', {idx}" + ')">\n'
  174. html += f'<div class="card-number">#{idx}</div>\n'
  175. html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
  176. html += '</div>\n'
  177. html += '</div>\n'
  178. html += '</div>\n'
  179. # 目的点(左)
  180. if purpose_points:
  181. html += '<div class="target-group">\n'
  182. html += '<h4 class="group-title">目的点</h4>\n'
  183. html += '<div class="target-items">\n'
  184. for idx, point in enumerate(purpose_points, 1):
  185. # 确保 point 是字典
  186. if not isinstance(point, dict):
  187. continue
  188. # 使用提取的特征中的特征名称,每个特征名称一个卡片
  189. features = point.get('提取的特征', [])
  190. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  191. if feature_names:
  192. # 为每个特征名称创建一个独立ID的卡片
  193. for feature_idx, feature_name in enumerate(feature_names, 1):
  194. feature_id = f'purpose-{idx}-{feature_idx}'
  195. html += f'<div class="target-card purpose-card" data-type="purpose-substance" data-id="{feature_id}" onclick="selectLeftTarget(' + f"'purpose', '{idx}-{feature_idx}'" + ')">\n'
  196. html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
  197. html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
  198. html += '</div>\n'
  199. else:
  200. # 如果没有特征,使用原始的目的点文本
  201. display_text = point.get('目的点', '')
  202. html += f'<div class="target-card purpose-card" data-type="purpose-substance" data-id="purpose-{idx}" onclick="selectLeftTarget(' + f"'purpose', {idx}" + ')">\n'
  203. html += f'<div class="card-number">#{idx}</div>\n'
  204. html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
  205. html += '</div>\n'
  206. html += '</div>\n'
  207. html += '</div>\n'
  208. html += '</div>\n'
  209. # 第2列:实质点
  210. html += '<div class="tab5-column tab5-substances">\n'
  211. html += '<h3 class="panel-title">实质点</h3>\n'
  212. # 具体元素
  213. if concrete_elements:
  214. html += '<div class="substance-group">\n'
  215. html += '<h4 class="group-title">具体元素</h4>\n'
  216. html += '<div class="substance-items">\n'
  217. for substance in concrete_elements:
  218. html += render_substance_card(substance, 'concrete-element')
  219. html += '</div>\n'
  220. html += '</div>\n'
  221. # 具象概念
  222. if concrete_concepts:
  223. html += '<div class="substance-group">\n'
  224. html += '<h4 class="group-title">具象概念</h4>\n'
  225. html += '<div class="substance-items">\n'
  226. for substance in concrete_concepts:
  227. html += render_substance_card(substance, 'concrete-concept')
  228. html += '</div>\n'
  229. html += '</div>\n'
  230. # 隐含概念
  231. if implicit_concepts:
  232. html += '<div class="substance-group">\n'
  233. html += '<h4 class="group-title">隐含概念</h4>\n'
  234. html += '<div class="substance-items">\n'
  235. for substance in implicit_concepts:
  236. html += render_substance_card(substance, 'implicit-concept')
  237. html += '</div>\n'
  238. html += '</div>\n'
  239. # 抽象概念
  240. if abstract_concepts:
  241. html += '<div class="substance-group">\n'
  242. html += '<h4 class="group-title">抽象概念</h4>\n'
  243. html += '<div class="substance-items">\n'
  244. for substance in abstract_concepts:
  245. html += render_substance_card(substance, 'abstract-concept')
  246. html += '</div>\n'
  247. html += '</div>\n'
  248. html += '</div>\n'
  249. # 第3列:形式点
  250. html += '<div class="tab5-column tab5-forms">\n'
  251. html += '<h3 class="panel-title">形式点</h3>\n'
  252. # 具体元素形式
  253. if concrete_element_forms:
  254. html += '<div class="form-group">\n'
  255. html += '<h4 class="group-title">具体元素形式</h4>\n'
  256. html += '<div class="form-items">\n'
  257. for form in concrete_element_forms:
  258. html += render_form_card(form, 'concrete-element-form')
  259. html += '</div>\n'
  260. html += '</div>\n'
  261. # 具象概念形式
  262. if concrete_concept_forms:
  263. html += '<div class="form-group">\n'
  264. html += '<h4 class="group-title">具象概念形式</h4>\n'
  265. html += '<div class="form-items">\n'
  266. for form in concrete_concept_forms:
  267. html += render_form_card(form, 'concrete-concept-form')
  268. html += '</div>\n'
  269. html += '</div>\n'
  270. # 整体形式
  271. if overall_forms:
  272. html += '<div class="form-group">\n'
  273. html += '<h4 class="group-title">整体形式</h4>\n'
  274. html += '<div class="form-items">\n'
  275. for form in overall_forms:
  276. html += render_form_card(form, 'overall-form')
  277. html += '</div>\n'
  278. html += '</div>\n'
  279. html += '</div>\n'
  280. # 第4列:右侧选题点(来自形式点的支撑关系)
  281. html += '<div class="tab5-column tab5-right-targets">\n'
  282. html += '<h3 class="panel-title">选题点<br/><span style="font-size:0.8em;font-weight:normal;color:#6c757d;">(形式支撑)</span></h3>\n'
  283. # 灵感点(右)
  284. if inspiration_points:
  285. html += '<div class="target-group">\n'
  286. html += '<h4 class="group-title">灵感点</h4>\n'
  287. html += '<div class="target-items">\n'
  288. for idx, point in enumerate(inspiration_points, 1):
  289. # 确保 point 是字典
  290. if not isinstance(point, dict):
  291. continue
  292. # 使用提取的特征中的特征名称,每个特征名称一个卡片
  293. features = point.get('提取的特征', [])
  294. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  295. if feature_names:
  296. # 为每个特征名称创建一个独立ID的卡片
  297. for feature_idx, feature_name in enumerate(feature_names, 1):
  298. feature_id = f'inspiration-{idx}-{feature_idx}'
  299. html += f'<div class="target-card inspiration-card" data-type="inspiration-form" data-id="{feature_id}" onclick="selectRightTarget(' + f"'inspiration', '{idx}-{feature_idx}'" + ')">\n'
  300. html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
  301. html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
  302. html += '</div>\n'
  303. else:
  304. # 如果没有特征,使用原始的灵感点文本
  305. display_text = point.get('灵感点', '')
  306. html += f'<div class="target-card inspiration-card" data-type="inspiration-form" data-id="inspiration-{idx}" onclick="selectRightTarget(' + f"'inspiration', {idx}" + ')">\n'
  307. html += f'<div class="card-number">#{idx}</div>\n'
  308. html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
  309. html += '</div>\n'
  310. html += '</div>\n'
  311. html += '</div>\n'
  312. # 关键点(右)
  313. if key_points:
  314. html += '<div class="target-group">\n'
  315. html += '<h4 class="group-title">关键点</h4>\n'
  316. html += '<div class="target-items">\n'
  317. for idx, point in enumerate(key_points, 1):
  318. # 确保 point 是字典
  319. if not isinstance(point, dict):
  320. continue
  321. # 使用提取的特征中的特征名称,每个特征名称一个卡片
  322. features = point.get('提取的特征', [])
  323. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  324. if feature_names:
  325. # 为每个特征名称创建一个独立ID的卡片
  326. for feature_idx, feature_name in enumerate(feature_names, 1):
  327. feature_id = f'keypoint-{idx}-{feature_idx}'
  328. html += f'<div class="target-card keypoint-card" data-type="keypoint-form" data-id="{feature_id}" onclick="selectRightTarget(' + f"'keypoint', '{idx}-{feature_idx}'" + ')">\n'
  329. html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
  330. html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
  331. html += '</div>\n'
  332. else:
  333. # 如果没有特征,使用原始的关键点文本
  334. display_text = point.get('关键点', '')
  335. html += f'<div class="target-card keypoint-card" data-type="keypoint-form" data-id="keypoint-{idx}" onclick="selectRightTarget(' + f"'keypoint', {idx}" + ')">\n'
  336. html += f'<div class="card-number">#{idx}</div>\n'
  337. html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
  338. html += '</div>\n'
  339. html += '</div>\n'
  340. html += '</div>\n'
  341. # 目的点(右)
  342. if purpose_points:
  343. html += '<div class="target-group">\n'
  344. html += '<h4 class="group-title">目的点</h4>\n'
  345. html += '<div class="target-items">\n'
  346. for idx, point in enumerate(purpose_points, 1):
  347. # 确保 point 是字典
  348. if not isinstance(point, dict):
  349. continue
  350. # 使用提取的特征中的特征名称,每个特征名称一个卡片
  351. features = point.get('提取的特征', [])
  352. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  353. if feature_names:
  354. # 为每个特征名称创建一个独立ID的卡片
  355. for feature_idx, feature_name in enumerate(feature_names, 1):
  356. feature_id = f'purpose-{idx}-{feature_idx}'
  357. html += f'<div class="target-card purpose-card" data-type="purpose-form" data-id="{feature_id}" onclick="selectRightTarget(' + f"'purpose', '{idx}-{feature_idx}'" + ')">\n'
  358. html += f'<div class="card-number">#{idx}-{feature_idx}</div>\n'
  359. html += f'<div class="card-text">{html_module.escape(feature_name)}</div>\n'
  360. html += '</div>\n'
  361. else:
  362. # 如果没有特征,使用原始的目的点文本
  363. display_text = point.get('目的点', '')
  364. html += f'<div class="target-card purpose-card" data-type="purpose-form" data-id="purpose-{idx}" onclick="selectRightTarget(' + f"'purpose', {idx}" + ')">\n'
  365. html += f'<div class="card-number">#{idx}</div>\n'
  366. html += f'<div class="card-text">{html_module.escape(display_text)}</div>\n'
  367. html += '</div>\n'
  368. html += '</div>\n'
  369. html += '</div>\n'
  370. html += '</div>\n'
  371. html += '</div>\n'
  372. # 嵌入关系数据
  373. html += '<script>\n'
  374. html += f'const tab5Relationships = {json.dumps(relationships, ensure_ascii=False)};\n'
  375. html += '</script>\n'
  376. html += '</div>\n'
  377. return html
  378. def render_substance_card(substance: Dict[str, Any], css_class: str) -> str:
  379. """渲染实质点卡片"""
  380. substance_id = substance.get('id', '')
  381. substance_name = substance.get('名称', '')
  382. description = substance.get('描述', '')
  383. html = f'<div class="substance-card {css_class}" data-id="{html_module.escape(substance_id)}" onclick="selectSubstance(' + f"'{substance_id}'" + ')">\n'
  384. html += f'<div class="card-header">\n'
  385. html += f'<div class="card-id">#{html_module.escape(substance_id)}</div>\n'
  386. html += f'<div class="card-name">{html_module.escape(substance_name)}</div>\n'
  387. html += '</div>\n'
  388. if description:
  389. html += f'<div class="card-description">{html_module.escape(description[:50])}{"..." if len(description) > 50 else ""}</div>\n'
  390. html += '</div>\n'
  391. return html
  392. def render_form_card(form: Dict[str, Any], css_class: str) -> str:
  393. """渲染形式点卡片"""
  394. form_id = form.get('id', '')
  395. form_name = form.get('名称', '')
  396. description = form.get('描述', '')
  397. weight_score = form.get('权重分')
  398. html = f'<div class="form-card {css_class}" data-id="{html_module.escape(form_id)}" onclick="selectForm(' + f"'{form_id}'" + ')">\n'
  399. html += f'<div class="card-header">\n'
  400. html += f'<div class="card-id">#{html_module.escape(form_id)}</div>\n'
  401. html += f'<div class="card-name">{html_module.escape(form_name)}</div>\n'
  402. if weight_score is not None:
  403. html += f'<div class="card-weight" style="font-size: 11px; color: #666; margin-top: 2px;">权重分: {weight_score:.1f}</div>\n'
  404. html += '</div>\n'
  405. if description:
  406. html += f'<div class="card-description">{html_module.escape(description[:50])}{"..." if len(description) > 50 else ""}</div>\n'
  407. html += '</div>\n'
  408. return html
  409. def build_bidirectional_relationships(
  410. concrete_elements: List[Dict],
  411. concrete_concepts: List[Dict],
  412. implicit_concepts: List[Dict],
  413. abstract_concepts: List[Dict],
  414. concrete_element_forms: List[Dict],
  415. concrete_concept_forms: List[Dict],
  416. overall_forms: List[Dict],
  417. inspiration_points: List[Dict],
  418. purpose_points: List[Dict],
  419. key_points: List[Dict]
  420. ) -> Dict[str, Any]:
  421. """
  422. 构建双向支撑关系数据
  423. 返回结构:
  424. {
  425. "substance_to_target": {
  426. "substance_id": {
  427. "inspiration": [{target_id, score, ...}],
  428. "purpose": [...],
  429. "keypoint": [...]
  430. }
  431. },
  432. "form_to_target": {
  433. "form_id": {
  434. "inspiration": [{target_id, score, ...}],
  435. "purpose": [...],
  436. "keypoint": [...]
  437. }
  438. },
  439. "target_from_substance": {
  440. "inspiration-1": [substance_ids with scores],
  441. "keypoint-1": [...],
  442. "purpose-1": [...]
  443. },
  444. "target_from_form": {
  445. "inspiration-1": [form_ids with scores],
  446. ...
  447. },
  448. "form_to_substance": {
  449. "form_id": [substance_ids],
  450. ...
  451. },
  452. "substance_from_form": {
  453. "substance_id": [form_ids],
  454. ...
  455. }
  456. }
  457. """
  458. relationships = {
  459. "substance_to_target": {},
  460. "form_to_target": {},
  461. "target_from_substance": {},
  462. "target_from_form": {},
  463. "form_to_substance": {}, # 新增:形式点→实质点
  464. "substance_from_form": {} # 新增:实质点←形式点(反向)
  465. }
  466. # 合并所有实质点和形式点
  467. all_substances = concrete_elements + concrete_concepts + implicit_concepts + abstract_concepts
  468. all_forms = concrete_element_forms + concrete_concept_forms + overall_forms
  469. # 注意:优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段(兼容旧数据)
  470. # 这两个字段内部存储的都是意图支撑数据,每个项目就是一个支撑点
  471. # 不再需要阈值过滤,所有支撑点都会显示连线
  472. # 1. 构建实质点到选题点的关系(基于意图支撑数据)
  473. for substance in all_substances:
  474. substance_id = substance.get('id', '')
  475. if not substance_id:
  476. continue
  477. dimension_2 = substance.get('维度', {}).get('二级', '')
  478. intention_support = get_intent_support_data(substance)
  479. relationships["substance_to_target"][substance_id] = {
  480. "name": substance.get('名称', ''),
  481. "type": dimension_2,
  482. "inspiration": [],
  483. "purpose": [],
  484. "keypoint": []
  485. }
  486. # 处理灵感点(意图支撑)
  487. for support_item in intention_support.get('灵感点', []):
  488. point_name = support_item.get('点')
  489. if not point_name:
  490. continue
  491. # 在灵感点列表中查找匹配的点,以便复用Tab1中的卡片id/特征结构
  492. idx = next(
  493. (i for i, p in enumerate(inspiration_points, 1)
  494. if isinstance(p, dict) and p.get('灵感点') == point_name),
  495. None
  496. )
  497. if idx is None:
  498. continue
  499. point = inspiration_points[idx - 1]
  500. features = point.get('提取的特征', [])
  501. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  502. if feature_names:
  503. # 为每个特征创建独立的关系
  504. for feature_idx in range(1, len(feature_names) + 1):
  505. target_id = f'inspiration-{idx}-{feature_idx}'
  506. relationships["substance_to_target"][substance_id]["inspiration"].append({
  507. "target_id": target_id,
  508. "point": point_name,
  509. "support_reason": support_item.get('支撑理由', '')
  510. })
  511. # 反向关系
  512. if target_id not in relationships["target_from_substance"]:
  513. relationships["target_from_substance"][target_id] = []
  514. relationships["target_from_substance"][target_id].append({
  515. "substance_id": substance_id,
  516. "name": substance.get('名称', ''),
  517. "type": dimension_2,
  518. "support_reason": support_item.get('支撑理由', '')
  519. })
  520. else:
  521. # 没有特征,使用主索引
  522. target_id = f'inspiration-{idx}'
  523. relationships["substance_to_target"][substance_id]["inspiration"].append({
  524. "target_id": target_id,
  525. "point": point_name,
  526. "support_reason": support_item.get('支撑理由', '')
  527. })
  528. # 反向关系
  529. if target_id not in relationships["target_from_substance"]:
  530. relationships["target_from_substance"][target_id] = []
  531. relationships["target_from_substance"][target_id].append({
  532. "substance_id": substance_id,
  533. "name": substance.get('名称', ''),
  534. "type": dimension_2,
  535. "support_reason": support_item.get('支撑理由', '')
  536. })
  537. # 处理目的点(意图支撑)
  538. for support_item in intention_support.get('目的点', []):
  539. point_name = support_item.get('点')
  540. if not point_name:
  541. continue
  542. idx = next(
  543. (i for i, p in enumerate(purpose_points, 1)
  544. if isinstance(p, dict) and p.get('目的点') == point_name),
  545. None
  546. )
  547. if idx is None:
  548. continue
  549. point = purpose_points[idx - 1]
  550. features = point.get('提取的特征', [])
  551. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  552. if feature_names:
  553. for feature_idx in range(1, len(feature_names) + 1):
  554. target_id = f'purpose-{idx}-{feature_idx}'
  555. relationships["substance_to_target"][substance_id]["purpose"].append({
  556. "target_id": target_id,
  557. "point": point_name,
  558. "support_reason": support_item.get('支撑理由', '')
  559. })
  560. if target_id not in relationships["target_from_substance"]:
  561. relationships["target_from_substance"][target_id] = []
  562. relationships["target_from_substance"][target_id].append({
  563. "substance_id": substance_id,
  564. "name": substance.get('名称', ''),
  565. "type": dimension_2,
  566. "support_reason": support_item.get('支撑理由', '')
  567. })
  568. else:
  569. target_id = f'purpose-{idx}'
  570. relationships["substance_to_target"][substance_id]["purpose"].append({
  571. "target_id": target_id,
  572. "point": point_name,
  573. "support_reason": support_item.get('支撑理由', '')
  574. })
  575. if target_id not in relationships["target_from_substance"]:
  576. relationships["target_from_substance"][target_id] = []
  577. relationships["target_from_substance"][target_id].append({
  578. "substance_id": substance_id,
  579. "name": substance.get('名称', ''),
  580. "type": dimension_2,
  581. "support_reason": support_item.get('支撑理由', '')
  582. })
  583. # 处理关键点(意图支撑)
  584. for support_item in intention_support.get('关键点', []):
  585. point_name = support_item.get('点')
  586. if not point_name:
  587. continue
  588. idx = next(
  589. (i for i, p in enumerate(key_points, 1)
  590. if isinstance(p, dict) and p.get('关键点') == point_name),
  591. None
  592. )
  593. if idx is None:
  594. continue
  595. point = key_points[idx - 1]
  596. features = point.get('提取的特征', [])
  597. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  598. if feature_names:
  599. for feature_idx in range(1, len(feature_names) + 1):
  600. target_id = f'keypoint-{idx}-{feature_idx}'
  601. relationships["substance_to_target"][substance_id]["keypoint"].append({
  602. "target_id": target_id,
  603. "point": point_name,
  604. "support_reason": support_item.get('支撑理由', '')
  605. })
  606. if target_id not in relationships["target_from_substance"]:
  607. relationships["target_from_substance"][target_id] = []
  608. relationships["target_from_substance"][target_id].append({
  609. "substance_id": substance_id,
  610. "name": substance.get('名称', ''),
  611. "type": dimension_2,
  612. "support_reason": support_item.get('支撑理由', '')
  613. })
  614. else:
  615. target_id = f'keypoint-{idx}'
  616. relationships["substance_to_target"][substance_id]["keypoint"].append({
  617. "target_id": target_id,
  618. "point": point_name,
  619. "support_reason": support_item.get('支撑理由', '')
  620. })
  621. if target_id not in relationships["target_from_substance"]:
  622. relationships["target_from_substance"][target_id] = []
  623. relationships["target_from_substance"][target_id].append({
  624. "substance_id": substance_id,
  625. "name": substance.get('名称', ''),
  626. "type": dimension_2,
  627. "support_reason": support_item.get('支撑理由', '')
  628. })
  629. # 2. 构建形式点到选题点的关系
  630. for form in all_forms:
  631. form_id = form.get('id', '')
  632. if not form_id:
  633. continue
  634. dimension_2 = form.get('维度', {}).get('二级', '')
  635. # 优先使用"意图支撑"字段,如果没有则使用"多维度评分"字段
  636. intent_support_data = get_intent_support_data(form)
  637. relationships["form_to_target"][form_id] = {
  638. "name": form.get('名称', ''),
  639. "type": dimension_2,
  640. "inspiration": [],
  641. "purpose": [],
  642. "keypoint": []
  643. }
  644. # 处理灵感点(每个项目就是一个支撑点)
  645. if '灵感点' in intent_support_data:
  646. for support_point in intent_support_data['灵感点']:
  647. if not isinstance(support_point, dict):
  648. continue
  649. # 每个项目就是一个支撑点,直接使用
  650. point_name = support_point.get('点', '')
  651. if not point_name:
  652. continue
  653. # 根据点名称找到对应的灵感点索引
  654. point_idx = None
  655. for idx, point in enumerate(inspiration_points, 1):
  656. if point.get('灵感点') == point_name or point.get('名称') == point_name:
  657. point_idx = idx
  658. break
  659. if point_idx and point_idx <= len(inspiration_points):
  660. point = inspiration_points[point_idx - 1]
  661. features = point.get('提取的特征', [])
  662. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  663. if feature_names:
  664. # 为每个特征创建独立的关系
  665. for feature_idx in range(1, len(feature_names) + 1):
  666. target_id = f'inspiration-{point_idx}-{feature_idx}'
  667. relationships["form_to_target"][form_id]["inspiration"].append({
  668. "target_id": target_id,
  669. "point": point_name,
  670. "support_reason": support_point.get('支撑理由', '')
  671. })
  672. # 反向关系
  673. if target_id not in relationships["target_from_form"]:
  674. relationships["target_from_form"][target_id] = []
  675. relationships["target_from_form"][target_id].append({
  676. "form_id": form_id,
  677. "name": form.get('名称', ''),
  678. "type": dimension_2,
  679. "support_reason": support_point.get('支撑理由', '')
  680. })
  681. else:
  682. # 没有特征,使用主索引
  683. target_id = f'inspiration-{point_idx}'
  684. relationships["form_to_target"][form_id]["inspiration"].append({
  685. "target_id": target_id,
  686. "point": point_name,
  687. "support_reason": support_point.get('支撑理由', '')
  688. })
  689. # 反向关系
  690. if target_id not in relationships["target_from_form"]:
  691. relationships["target_from_form"][target_id] = []
  692. relationships["target_from_form"][target_id].append({
  693. "form_id": form_id,
  694. "name": form.get('名称', ''),
  695. "type": dimension_2,
  696. "support_reason": support_point.get('支撑理由', '')
  697. })
  698. # 处理目的点(每个项目就是一个支撑点)
  699. if '目的点' in intent_support_data:
  700. for support_point in intent_support_data['目的点']:
  701. if not isinstance(support_point, dict):
  702. continue
  703. # 每个项目就是一个支撑点,直接使用
  704. point_name = support_point.get('点', '')
  705. if not point_name:
  706. continue
  707. # 根据点名称找到对应的目的点索引
  708. point_idx = None
  709. for idx, point in enumerate(purpose_points, 1):
  710. if point.get('目的点') == point_name or point.get('名称') == point_name:
  711. point_idx = idx
  712. break
  713. if point_idx and point_idx <= len(purpose_points):
  714. point = purpose_points[point_idx - 1]
  715. features = point.get('提取的特征', [])
  716. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  717. if feature_names:
  718. for feature_idx in range(1, len(feature_names) + 1):
  719. target_id = f'purpose-{point_idx}-{feature_idx}'
  720. relationships["form_to_target"][form_id]["purpose"].append({
  721. "target_id": target_id,
  722. "point": point_name,
  723. "support_reason": support_point.get('支撑理由', '')
  724. })
  725. if target_id not in relationships["target_from_form"]:
  726. relationships["target_from_form"][target_id] = []
  727. relationships["target_from_form"][target_id].append({
  728. "form_id": form_id,
  729. "name": form.get('名称', ''),
  730. "type": dimension_2,
  731. "support_reason": support_point.get('支撑理由', '')
  732. })
  733. else:
  734. target_id = f'purpose-{point_idx}'
  735. relationships["form_to_target"][form_id]["purpose"].append({
  736. "target_id": target_id,
  737. "point": point_name,
  738. "support_reason": support_point.get('支撑理由', '')
  739. })
  740. if target_id not in relationships["target_from_form"]:
  741. relationships["target_from_form"][target_id] = []
  742. relationships["target_from_form"][target_id].append({
  743. "form_id": form_id,
  744. "name": form.get('名称', ''),
  745. "type": dimension_2,
  746. "support_reason": support_point.get('支撑理由', '')
  747. })
  748. # 处理关键点(每个项目就是一个支撑点)
  749. if '关键点' in intent_support_data:
  750. for support_point in intent_support_data['关键点']:
  751. if not isinstance(support_point, dict):
  752. continue
  753. # 每个项目就是一个支撑点,直接使用
  754. point_name = support_point.get('点', '')
  755. if not point_name:
  756. continue
  757. # 根据点名称找到对应的关键点索引
  758. point_idx = None
  759. for idx, point in enumerate(key_points, 1):
  760. if point.get('关键点') == point_name or point.get('名称') == point_name:
  761. point_idx = idx
  762. break
  763. if point_idx and point_idx <= len(key_points):
  764. point = key_points[point_idx - 1]
  765. features = point.get('提取的特征', [])
  766. feature_names = [f.get('特征名称', '') for f in features if f.get('特征名称')]
  767. if feature_names:
  768. for feature_idx in range(1, len(feature_names) + 1):
  769. target_id = f'keypoint-{point_idx}-{feature_idx}'
  770. relationships["form_to_target"][form_id]["keypoint"].append({
  771. "target_id": target_id,
  772. "point": point_name,
  773. "support_reason": support_point.get('支撑理由', '')
  774. })
  775. if target_id not in relationships["target_from_form"]:
  776. relationships["target_from_form"][target_id] = []
  777. relationships["target_from_form"][target_id].append({
  778. "form_id": form_id,
  779. "name": form.get('名称', ''),
  780. "type": dimension_2,
  781. "support_reason": support_point.get('支撑理由', '')
  782. })
  783. else:
  784. target_id = f'keypoint-{point_idx}'
  785. relationships["form_to_target"][form_id]["keypoint"].append({
  786. "target_id": target_id,
  787. "point": point_name,
  788. "support_reason": support_point.get('支撑理由', '')
  789. })
  790. if target_id not in relationships["target_from_form"]:
  791. relationships["target_from_form"][target_id] = []
  792. relationships["target_from_form"][target_id].append({
  793. "form_id": form_id,
  794. "name": form.get('名称', ''),
  795. "type": dimension_2,
  796. "support_reason": support_point.get('支撑理由', '')
  797. })
  798. # 3. 构建形式点到实质点的支撑关系
  799. for form in all_forms:
  800. form_id = form.get('id', '')
  801. if not form_id:
  802. continue
  803. dimension_2 = form.get('维度', {}).get('二级', '')
  804. support_data = form.get('支撑', [])
  805. if not support_data:
  806. continue
  807. # 初始化形式点的支撑关系
  808. if form_id not in relationships["form_to_substance"]:
  809. relationships["form_to_substance"][form_id] = []
  810. # 支撑字段可能是列表或字典
  811. support_items = []
  812. if isinstance(support_data, list):
  813. # 列表格式:[{id, 名称}, ...]
  814. support_items = support_data
  815. elif isinstance(support_data, dict):
  816. # 字典格式:{'具体元素': [...], '具象概念': [...]}
  817. for _category, items in support_data.items():
  818. if isinstance(items, list):
  819. support_items.extend(items)
  820. for support_item in support_items:
  821. if not isinstance(support_item, dict):
  822. continue
  823. substance_id = support_item.get('id', '')
  824. substance_name = support_item.get('名称', '')
  825. if not substance_id:
  826. continue
  827. # 形式点 → 实质点
  828. relationships["form_to_substance"][form_id].append({
  829. "substance_id": substance_id,
  830. "name": substance_name
  831. })
  832. # 实质点 ← 形式点(反向)
  833. if substance_id not in relationships["substance_from_form"]:
  834. relationships["substance_from_form"][substance_id] = []
  835. relationships["substance_from_form"][substance_id].append({
  836. "form_id": form_id,
  837. "name": form.get('名称', ''),
  838. "type": dimension_2
  839. })
  840. return relationships