index.js 88 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { build } = require('esbuild');
  5. const { convertV8ToGraph } = require('./convert_v8_to_graph');
  6. const { convertV8ToGraphV2, convertV8ToGraphSimplified } = require('./convert_v8_to_graph_v3');
  7. // 读取命令行参数
  8. const args = process.argv.slice(2);
  9. if (args.length === 0) {
  10. console.error('Usage: node index.js <path-to-run_context.json> [output.html] [--simplified]');
  11. process.exit(1);
  12. }
  13. const inputFile = args[0];
  14. const outputFile = args[1] || 'query_graph_output.html';
  15. const useSimplified = args.includes('--simplified');
  16. // 读取输入数据
  17. const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
  18. // 检测数据格式并转换
  19. let data;
  20. if (inputData.rounds && inputData.o) {
  21. // v6.1.2.8 格式,需要转换
  22. console.log('✨ 检测到 v6.1.2.8 格式,正在转换为图结构...');
  23. // 尝试读取 search_results.json(兼容旧版本)
  24. let searchResults = null;
  25. const searchResultsPath = path.join(path.dirname(inputFile), 'search_results.json');
  26. if (fs.existsSync(searchResultsPath)) {
  27. console.log('📄 读取外部搜索结果数据(兼容模式)...');
  28. searchResults = JSON.parse(fs.readFileSync(searchResultsPath, 'utf-8'));
  29. } else {
  30. console.log('✅ 使用 run_context.json 中的内嵌搜索结果');
  31. }
  32. // 选择转换函数
  33. let graphData;
  34. let fullData = null; // 用于目录的完整数据
  35. if (useSimplified) {
  36. console.log('🎨 使用简化视图(合并query节点)');
  37. // 生成简化版用于画布
  38. graphData = convertV8ToGraphSimplified(inputData, searchResults);
  39. // 生成完整版用于目录
  40. const fullGraphData = convertV8ToGraphV2(inputData, searchResults);
  41. fullData = {
  42. nodes: fullGraphData.nodes,
  43. edges: fullGraphData.edges,
  44. iterations: fullGraphData.iterations
  45. };
  46. console.log(`✅ 简化版: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
  47. console.log(`📋 完整版(用于目录): ${Object.keys(fullData.nodes).length} 个节点`);
  48. } else {
  49. console.log('📊 使用详细视图(完整流程)');
  50. graphData = convertV8ToGraphV2(inputData, searchResults);
  51. console.log(`✅ 转换完成: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
  52. }
  53. data = {
  54. nodes: graphData.nodes,
  55. edges: graphData.edges,
  56. iterations: graphData.iterations,
  57. fullData: fullData // 传递完整数据
  58. };
  59. } else if (inputData.nodes && inputData.edges) {
  60. // v6.1.2.5 格式,直接使用
  61. console.log('✨ 检测到 v6.1.2.5 格式,直接使用');
  62. data = inputData;
  63. } else {
  64. console.error('❌ 无法识别的数据格式');
  65. process.exit(1);
  66. }
  67. // 创建临时 React 组件文件
  68. const reactComponentPath = path.join(__dirname, 'temp_flow_component_v2.jsx');
  69. const reactComponent = `
  70. import React, { useState, useCallback, useMemo, useEffect } from 'react';
  71. import { createRoot } from 'react-dom/client';
  72. import {
  73. ReactFlow,
  74. Controls,
  75. Background,
  76. useNodesState,
  77. useEdgesState,
  78. Handle,
  79. Position,
  80. useReactFlow,
  81. ReactFlowProvider,
  82. } from '@xyflow/react';
  83. import '@xyflow/react/dist/style.css';
  84. const data = ${JSON.stringify(data, null, 2)};
  85. // 根据节点类型获取边框颜色
  86. function getNodeTypeColor(type) {
  87. const typeColors = {
  88. 'root': '#6b21a8', // 紫色 - 根节点
  89. 'round': '#7c3aed', // 深紫 - Round节点
  90. 'step': '#f59e0b', // 橙色 - 步骤节点
  91. 'seg': '#10b981', // 绿色 - 分词
  92. 'q': '#3b82f6', // 蓝色 - Query
  93. 'sug': '#06b6d4', // 青色 - Sug建议词
  94. 'seed': '#84cc16', // 黄绿 - Seed
  95. 'add_word': '#22c55e', // 绿色 - 加词生成
  96. 'search_word': '#8b5cf6', // 紫色 - 搜索词
  97. 'post': '#ec4899', // 粉色 - 帖子
  98. 'filtered_sug': '#14b8a6',// 青绿 - 筛选的sug
  99. 'next_q': '#2563eb', // 深蓝 - 下轮查询
  100. 'next_seed': '#65a30d', // 深黄绿 - 下轮种子
  101. 'search': '#8b5cf6', // 深紫 - 搜索(兼容旧版)
  102. 'operation': '#f59e0b', // 橙色 - 操作节点(兼容旧版)
  103. 'query': '#3b82f6', // 蓝色 - 查询(兼容旧版)
  104. 'note': '#ec4899', // 粉色 - 帖子(兼容旧版)
  105. };
  106. return typeColors[type] || '#9ca3af';
  107. }
  108. // 查询节点组件 - 卡片样式
  109. function QueryNode({ id, data, sourcePosition, targetPosition }) {
  110. // 所有节点默认展开
  111. const expanded = true;
  112. // 获取节点类型颜色
  113. const typeColor = getNodeTypeColor(data.nodeType || 'query');
  114. return (
  115. <div>
  116. <Handle
  117. type="target"
  118. position={targetPosition || Position.Left}
  119. style={{ background: typeColor, width: 8, height: 8 }}
  120. />
  121. <div
  122. style={{
  123. padding: '12px',
  124. borderRadius: '8px',
  125. border: data.isHighlighted ? \`3px solid \${typeColor}\` :
  126. data.isCollapsed ? \`2px solid \${typeColor}\` :
  127. data.isSelected === false ? '2px dashed #d1d5db' :
  128. \`2px solid \${typeColor}\`,
  129. background: data.isHighlighted ? '#eef2ff' :
  130. data.isSelected === false ? '#f9fafb' : 'white',
  131. minWidth: '200px',
  132. maxWidth: '280px',
  133. boxShadow: data.isHighlighted ? '0 0 0 4px rgba(102, 126, 234, 0.25), 0 4px 16px rgba(102, 126, 234, 0.4)' :
  134. data.isCollapsed ? '0 4px 12px rgba(102, 126, 234, 0.15)' :
  135. data.level === 0 ? '0 4px 12px rgba(139, 92, 246, 0.15)' : '0 2px 6px rgba(0, 0, 0, 0.06)',
  136. transition: 'all 0.3s ease',
  137. cursor: 'pointer',
  138. position: 'relative',
  139. opacity: data.isSelected === false ? 0.6 : 1,
  140. }}
  141. >
  142. {/* 折叠当前节点按钮 - 左边 */}
  143. <div
  144. style={{
  145. position: 'absolute',
  146. top: '6px',
  147. left: '6px',
  148. width: '20px',
  149. height: '20px',
  150. borderRadius: '50%',
  151. background: '#f59e0b',
  152. color: 'white',
  153. display: 'flex',
  154. alignItems: 'center',
  155. justifyContent: 'center',
  156. fontSize: '11px',
  157. fontWeight: 'bold',
  158. cursor: 'pointer',
  159. transition: 'all 0.2s ease',
  160. zIndex: 10,
  161. }}
  162. onClick={(e) => {
  163. e.stopPropagation();
  164. if (data.onHideSelf) {
  165. data.onHideSelf();
  166. }
  167. }}
  168. onMouseEnter={(e) => {
  169. e.currentTarget.style.background = '#d97706';
  170. }}
  171. onMouseLeave={(e) => {
  172. e.currentTarget.style.background = '#f59e0b';
  173. }}
  174. title="隐藏当前节点"
  175. >
  176. ×
  177. </div>
  178. {/* 聚焦按钮 - 右上角 */}
  179. <div
  180. style={{
  181. position: 'absolute',
  182. top: '6px',
  183. right: '6px',
  184. width: '20px',
  185. height: '20px',
  186. borderRadius: '50%',
  187. background: data.isFocused ? '#10b981' : '#e5e7eb',
  188. color: data.isFocused ? 'white' : '#6b7280',
  189. display: 'flex',
  190. alignItems: 'center',
  191. justifyContent: 'center',
  192. fontSize: '11px',
  193. fontWeight: 'bold',
  194. cursor: 'pointer',
  195. transition: 'all 0.2s ease',
  196. zIndex: 10,
  197. }}
  198. onClick={(e) => {
  199. e.stopPropagation();
  200. if (data.onFocus) {
  201. data.onFocus();
  202. }
  203. }}
  204. onMouseEnter={(e) => {
  205. if (!data.isFocused) {
  206. e.currentTarget.style.background = '#d1d5db';
  207. }
  208. }}
  209. onMouseLeave={(e) => {
  210. if (!data.isFocused) {
  211. e.currentTarget.style.background = '#e5e7eb';
  212. }
  213. }}
  214. title={data.isFocused ? '取消聚焦' : '聚焦到此节点'}
  215. >
  216. 🎯
  217. </div>
  218. {/* 折叠/展开子节点按钮 - 右边第二个位置 */}
  219. {data.hasChildren && (
  220. <div
  221. style={{
  222. position: 'absolute',
  223. top: '6px',
  224. right: '30px',
  225. width: '20px',
  226. height: '20px',
  227. borderRadius: '50%',
  228. background: data.isCollapsed ? '#667eea' : '#e5e7eb',
  229. color: data.isCollapsed ? 'white' : '#6b7280',
  230. display: 'flex',
  231. alignItems: 'center',
  232. justifyContent: 'center',
  233. fontSize: '11px',
  234. fontWeight: 'bold',
  235. cursor: 'pointer',
  236. transition: 'all 0.2s ease',
  237. zIndex: 10,
  238. }}
  239. onClick={(e) => {
  240. e.stopPropagation();
  241. data.onToggleCollapse();
  242. }}
  243. title={data.isCollapsed ? '展开子节点' : '折叠子节点'}
  244. >
  245. {data.isCollapsed ? '+' : '−'}
  246. </div>
  247. )}
  248. {/* 卡片内容 */}
  249. <div>
  250. {/* 标题行 */}
  251. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px', paddingLeft: '24px', paddingRight: data.hasChildren ? '54px' : '28px' }}>
  252. <div style={{ flex: 1 }}>
  253. <div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
  254. <div style={{
  255. fontSize: '13px',
  256. fontWeight: data.level === 0 ? '700' : '600',
  257. color: data.level === 0 ? '#6b21a8' : '#1f2937',
  258. lineHeight: '1.3',
  259. flex: 1,
  260. }}>
  261. {data.title}
  262. </div>
  263. {data.isSelected === false && (
  264. <div style={{
  265. fontSize: '9px',
  266. padding: '1px 4px',
  267. borderRadius: '3px',
  268. background: '#fee2e2',
  269. color: '#991b1b',
  270. fontWeight: '500',
  271. flexShrink: 0,
  272. }}>
  273. 未选中
  274. </div>
  275. )}
  276. </div>
  277. </div>
  278. </div>
  279. {/* 展开的详细信息 - 始终显示 */}
  280. <div style={{ fontSize: '11px', lineHeight: 1.4 }}>
  281. <div style={{ display: 'flex', gap: '4px', marginBottom: '6px', flexWrap: 'wrap' }}>
  282. <span style={{
  283. display: 'inline-block',
  284. padding: '1px 6px',
  285. borderRadius: '10px',
  286. background: '#eff6ff',
  287. color: '#3b82f6',
  288. fontSize: '10px',
  289. fontWeight: '500',
  290. }}>
  291. Lv.{data.level}
  292. </span>
  293. <span style={{
  294. display: 'inline-block',
  295. padding: '1px 6px',
  296. borderRadius: '10px',
  297. background: '#f0fdf4',
  298. color: '#16a34a',
  299. fontSize: '10px',
  300. fontWeight: '500',
  301. }}>
  302. {data.score}
  303. </span>
  304. {data.strategy && data.strategy !== 'root' && (
  305. <span style={{
  306. display: 'inline-block',
  307. padding: '1px 6px',
  308. borderRadius: '10px',
  309. background: '#fef3c7',
  310. color: '#92400e',
  311. fontSize: '10px',
  312. fontWeight: '500',
  313. }}>
  314. {data.strategy}
  315. </span>
  316. )}
  317. {(data.typeLabel || data.type_label) && (
  318. <span style={{
  319. display: 'inline-block',
  320. padding: '1px 6px',
  321. borderRadius: '10px',
  322. background: '#fce7f3',
  323. color: '#9f1239',
  324. fontSize: '10px',
  325. fontWeight: '500',
  326. }}>
  327. {data.typeLabel || data.type_label}
  328. </span>
  329. )}
  330. </div>
  331. {data.parent && (
  332. <div style={{ color: '#6b7280', fontSize: '10px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #f3f4f6' }}>
  333. <strong>Parent:</strong> {data.parent}
  334. </div>
  335. )}
  336. {data.selectedWord && (
  337. <div style={{
  338. marginTop: '6px',
  339. paddingTop: '6px',
  340. borderTop: '1px solid #f3f4f6',
  341. fontSize: '10px',
  342. color: '#6b7280',
  343. lineHeight: '1.5',
  344. }}>
  345. <strong style={{ color: '#4b5563' }}>选择词:</strong>
  346. <span style={{ marginLeft: '4px', color: '#3b82f6', fontWeight: '500' }}>{data.selectedWord}</span>
  347. {data.seed_score !== undefined && (
  348. <div style={{ marginTop: '4px' }}>
  349. <strong style={{ color: '#4b5563' }}>种子得分:</strong>
  350. <span style={{ marginLeft: '4px', color: '#16a34a', fontWeight: '500' }}>
  351. {typeof data.seed_score === 'number' ? data.seed_score.toFixed(2) : data.seed_score}
  352. </span>
  353. </div>
  354. )}
  355. </div>
  356. )}
  357. {data.evaluationReason && (
  358. <div style={{
  359. marginTop: '6px',
  360. paddingTop: '6px',
  361. borderTop: '1px solid #f3f4f6',
  362. fontSize: '10px',
  363. color: '#6b7280',
  364. lineHeight: '1.5',
  365. }}>
  366. <strong style={{ color: '#4b5563' }}>评估:</strong>
  367. <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
  368. </div>
  369. )}
  370. {data.occurrences && data.occurrences.length > 1 && (
  371. <div style={{
  372. marginTop: '6px',
  373. paddingTop: '6px',
  374. borderTop: '1px solid #f3f4f6',
  375. fontSize: '10px',
  376. color: '#6b7280',
  377. }}>
  378. <strong style={{ color: '#4b5563' }}>演化历史 ({data.occurrences.length}次):</strong>
  379. <div style={{ marginTop: '4px' }}>
  380. {data.occurrences.map((occ, idx) => (
  381. <div key={idx} style={{ marginTop: '2px', paddingLeft: '8px' }}>
  382. <span style={{ color: '#3b82f6', fontWeight: '500' }}>R{occ.round}</span>
  383. {' · '}
  384. <span>{occ.strategy}</span>
  385. {occ.score !== undefined && (
  386. <span style={{ color: '#16a34a', marginLeft: '4px' }}>
  387. ({typeof occ.score === 'number' ? occ.score.toFixed(2) : occ.score})
  388. </span>
  389. )}
  390. </div>
  391. ))}
  392. </div>
  393. </div>
  394. )}
  395. {data.hasSearchResults && (
  396. <div style={{
  397. marginTop: '6px',
  398. paddingTop: '6px',
  399. borderTop: '1px solid #f3f4f6',
  400. fontSize: '10px',
  401. background: '#fef3c7',
  402. padding: '4px 6px',
  403. borderRadius: '4px',
  404. color: '#92400e',
  405. fontWeight: '500',
  406. }}>
  407. 🔍 找到 {data.postCount} 个帖子
  408. </div>
  409. )}
  410. </div>
  411. </div>
  412. </div>
  413. <Handle
  414. type="source"
  415. position={sourcePosition || Position.Right}
  416. style={{ background: '#667eea', width: 8, height: 8 }}
  417. />
  418. </div>
  419. );
  420. }
  421. // 笔记节点组件 - 卡片样式,带轮播图
  422. function NoteNode({ id, data, sourcePosition, targetPosition }) {
  423. const [currentImageIndex, setCurrentImageIndex] = useState(0);
  424. const expanded = true;
  425. const hasImages = data.imageList && data.imageList.length > 0;
  426. const nextImage = (e) => {
  427. e.stopPropagation();
  428. if (hasImages) {
  429. setCurrentImageIndex((prev) => (prev + 1) % data.imageList.length);
  430. }
  431. };
  432. const prevImage = (e) => {
  433. e.stopPropagation();
  434. if (hasImages) {
  435. setCurrentImageIndex((prev) => (prev - 1 + data.imageList.length) % data.imageList.length);
  436. }
  437. };
  438. return (
  439. <div>
  440. <Handle
  441. type="target"
  442. position={targetPosition || Position.Left}
  443. style={{ background: '#ec4899', width: 8, height: 8 }}
  444. />
  445. <div
  446. style={{
  447. padding: '14px',
  448. borderRadius: '20px',
  449. border: data.isHighlighted ? '3px solid #ec4899' : '2px solid #fce7f3',
  450. background: data.isHighlighted ? '#eef2ff' : 'white',
  451. minWidth: '220px',
  452. maxWidth: '300px',
  453. boxShadow: data.isHighlighted ? '0 0 0 4px rgba(236, 72, 153, 0.25), 0 4px 16px rgba(236, 72, 153, 0.4)' : '0 4px 12px rgba(236, 72, 153, 0.15)',
  454. transition: 'all 0.3s ease',
  455. cursor: 'pointer',
  456. }}
  457. >
  458. {/* 笔记标题 */}
  459. <div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '8px' }}>
  460. <div style={{ flex: 1 }}>
  461. <div style={{
  462. fontSize: '13px',
  463. fontWeight: '600',
  464. color: '#831843',
  465. lineHeight: '1.4',
  466. marginBottom: '4px',
  467. }}>
  468. {data.title}
  469. </div>
  470. </div>
  471. </div>
  472. {/* 轮播图 */}
  473. {hasImages && (
  474. <div style={{
  475. position: 'relative',
  476. marginBottom: '8px',
  477. borderRadius: '12px',
  478. overflow: 'hidden',
  479. }}>
  480. <img
  481. src={data.imageList[currentImageIndex].image_url}
  482. alt={\`Image \${currentImageIndex + 1}\`}
  483. style={{
  484. width: '100%',
  485. height: '160px',
  486. objectFit: 'cover',
  487. display: 'block',
  488. }}
  489. onError={(e) => {
  490. e.target.style.display = 'none';
  491. }}
  492. />
  493. {data.imageList.length > 1 && (
  494. <>
  495. {/* 左右切换按钮 */}
  496. <button
  497. onClick={prevImage}
  498. style={{
  499. position: 'absolute',
  500. left: '4px',
  501. top: '50%',
  502. transform: 'translateY(-50%)',
  503. background: 'rgba(0, 0, 0, 0.5)',
  504. color: 'white',
  505. border: 'none',
  506. borderRadius: '50%',
  507. width: '24px',
  508. height: '24px',
  509. cursor: 'pointer',
  510. display: 'flex',
  511. alignItems: 'center',
  512. justifyContent: 'center',
  513. fontSize: '14px',
  514. }}
  515. >
  516. </button>
  517. <button
  518. onClick={nextImage}
  519. style={{
  520. position: 'absolute',
  521. right: '4px',
  522. top: '50%',
  523. transform: 'translateY(-50%)',
  524. background: 'rgba(0, 0, 0, 0.5)',
  525. color: 'white',
  526. border: 'none',
  527. borderRadius: '50%',
  528. width: '24px',
  529. height: '24px',
  530. cursor: 'pointer',
  531. display: 'flex',
  532. alignItems: 'center',
  533. justifyContent: 'center',
  534. fontSize: '14px',
  535. }}
  536. >
  537. </button>
  538. {/* 图片计数 */}
  539. <div style={{
  540. position: 'absolute',
  541. bottom: '4px',
  542. right: '4px',
  543. background: 'rgba(0, 0, 0, 0.6)',
  544. color: 'white',
  545. padding: '2px 6px',
  546. borderRadius: '10px',
  547. fontSize: '10px',
  548. }}>
  549. {currentImageIndex + 1}/{data.imageList.length}
  550. </div>
  551. </>
  552. )}
  553. </div>
  554. )}
  555. {/* 互动数据 */}
  556. {data.interact_info && (
  557. <div style={{
  558. display: 'flex',
  559. gap: '8px',
  560. marginBottom: '8px',
  561. flexWrap: 'wrap',
  562. fontSize: '11px',
  563. color: '#9f1239',
  564. }}>
  565. {data.interact_info.liked_count > 0 && (
  566. <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
  567. ❤️ {data.interact_info.liked_count}
  568. </span>
  569. )}
  570. {data.interact_info.collected_count > 0 && (
  571. <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
  572. ⭐ {data.interact_info.collected_count}
  573. </span>
  574. )}
  575. {data.interact_info.comment_count > 0 && (
  576. <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
  577. 💬 {data.interact_info.comment_count}
  578. </span>
  579. )}
  580. {data.interact_info.shared_count > 0 && (
  581. <span style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
  582. 🔗 {data.interact_info.shared_count}
  583. </span>
  584. )}
  585. </div>
  586. )}
  587. {/* 被哪些query找到 */}
  588. {data.foundByQueries && data.foundByQueries.length > 0 && (
  589. <div style={{
  590. marginBottom: '8px',
  591. padding: '6px 8px',
  592. background: '#f0fdf4',
  593. borderRadius: '6px',
  594. fontSize: '10px',
  595. }}>
  596. <strong style={{ color: '#16a34a' }}>🔍 被找到:</strong>
  597. <div style={{ marginTop: '4px', display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
  598. {data.foundByQueries.map((query, idx) => (
  599. <span key={idx} style={{
  600. display: 'inline-block',
  601. padding: '2px 6px',
  602. background: '#dcfce7',
  603. color: '#166534',
  604. borderRadius: '4px',
  605. fontSize: '9px',
  606. }}>
  607. {query}
  608. </span>
  609. ))}
  610. </div>
  611. {data.foundInRounds && data.foundInRounds.length > 0 && (
  612. <div style={{ marginTop: '4px', color: '#6b7280' }}>
  613. 出现在: Round {data.foundInRounds.join(', ')}
  614. </div>
  615. )}
  616. </div>
  617. )}
  618. {/* 标签 */}
  619. {(data.matchLevel || data.score) && (
  620. <div style={{ display: 'flex', gap: '6px', marginBottom: '8px', flexWrap: 'wrap' }}>
  621. {data.matchLevel && (
  622. <span style={{
  623. display: 'inline-block',
  624. padding: '2px 8px',
  625. borderRadius: '12px',
  626. background: '#fff1f2',
  627. color: '#be123c',
  628. fontSize: '10px',
  629. fontWeight: '500',
  630. }}>
  631. {data.matchLevel}
  632. </span>
  633. )}
  634. {data.score && (
  635. <span style={{
  636. display: 'inline-block',
  637. padding: '2px 8px',
  638. borderRadius: '12px',
  639. background: '#fff7ed',
  640. color: '#c2410c',
  641. fontSize: '10px',
  642. fontWeight: '500',
  643. }}>
  644. Score: {data.score}
  645. </span>
  646. )}
  647. </div>
  648. )}
  649. {/* 描述 */}
  650. {expanded && data.description && (
  651. <div style={{
  652. fontSize: '11px',
  653. color: '#9f1239',
  654. lineHeight: '1.5',
  655. paddingTop: '8px',
  656. borderTop: '1px solid #fbcfe8',
  657. }}>
  658. {data.description}
  659. </div>
  660. )}
  661. {/* 评估理由 */}
  662. {expanded && data.evaluationReason && (
  663. <div style={{
  664. fontSize: '10px',
  665. color: '#831843',
  666. lineHeight: '1.5',
  667. paddingTop: '8px',
  668. marginTop: '8px',
  669. borderTop: '1px solid #fbcfe8',
  670. }}>
  671. <strong style={{ color: '#9f1239' }}>评估:</strong>
  672. <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
  673. </div>
  674. )}
  675. </div>
  676. <Handle
  677. type="source"
  678. position={sourcePosition || Position.Right}
  679. style={{ background: '#ec4899', width: 8, height: 8 }}
  680. />
  681. </div>
  682. );
  683. }
  684. const nodeTypes = {
  685. query: QueryNode,
  686. note: NoteNode,
  687. };
  688. // 根据 score 获取颜色
  689. function getScoreColor(score) {
  690. if (score >= 0.7) return '#10b981'; // 绿色 - 高分
  691. if (score >= 0.4) return '#f59e0b'; // 橙色 - 中分
  692. return '#ef4444'; // 红色 - 低分
  693. }
  694. // 截断文本,保留头尾,中间显示省略号
  695. function truncateMiddle(text, maxLength = 20) {
  696. if (!text || text.length <= maxLength) return text;
  697. const headLength = Math.ceil(maxLength * 0.4);
  698. const tailLength = Math.floor(maxLength * 0.4);
  699. const head = text.substring(0, headLength);
  700. const tail = text.substring(text.length - tailLength);
  701. return \`\${head}...\${tail}\`;
  702. }
  703. // 根据策略获取颜色
  704. // 智能提取主要策略的辅助函数
  705. function getPrimaryStrategy(nodeData) {
  706. // 优先级1: 使用 primaryStrategy 字段
  707. if (nodeData.primaryStrategy) {
  708. return nodeData.primaryStrategy;
  709. }
  710. // 优先级2: 从 occurrences 数组中获取最新的策略
  711. if (nodeData.occurrences && Array.isArray(nodeData.occurrences) && nodeData.occurrences.length > 0) {
  712. const latestOccurrence = nodeData.occurrences[nodeData.occurrences.length - 1];
  713. if (latestOccurrence && latestOccurrence.strategy) {
  714. return latestOccurrence.strategy;
  715. }
  716. }
  717. // 优先级3: 拆分组合策略字符串,取第一个
  718. if (nodeData.strategy && typeof nodeData.strategy === 'string') {
  719. const strategies = nodeData.strategy.split(' + ');
  720. if (strategies.length > 0 && strategies[0]) {
  721. return strategies[0].trim();
  722. }
  723. }
  724. // 默认返回原始strategy或未知
  725. return nodeData.strategy || '未知';
  726. }
  727. function getStrategyColor(strategy) {
  728. const strategyColors = {
  729. '初始分词': '#10b981',
  730. '调用sug': '#06b6d4',
  731. '同义改写': '#f59e0b',
  732. '加词': '#3b82f6',
  733. '抽象改写': '#8b5cf6',
  734. '基于部分匹配改进': '#ec4899',
  735. '结果分支-抽象改写': '#a855f7',
  736. '结果分支-同义改写': '#fb923c',
  737. // v6.1.2.8 新增策略
  738. '原始问题': '#6b21a8',
  739. '来自分词': '#10b981',
  740. '加词生成': '#ef4444',
  741. '建议词': '#06b6d4',
  742. '执行搜索': '#8b5cf6',
  743. // 添加简化版本的策略映射
  744. '分词': '#10b981',
  745. '推荐词': '#06b6d4',
  746. };
  747. return strategyColors[strategy] || '#9ca3af';
  748. }
  749. // 树节点组件
  750. function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, onSelect }) {
  751. const hasChildren = children && children.length > 0;
  752. const score = node.data.score ? parseFloat(node.data.score) : 0;
  753. const strategy = getPrimaryStrategy(node.data); // 使用智能提取函数
  754. const strategyColor = getStrategyColor(strategy);
  755. const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型
  756. // 计算字体颜色:根据分数提升幅度判断
  757. let fontColor = '#374151'; // 默认颜色
  758. if (node.type === 'note') {
  759. fontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
  760. } else if (node.data.seed_score !== undefined) {
  761. const parentScore = parseFloat(node.data.seed_score);
  762. const gain = score - parentScore;
  763. fontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
  764. } else if (node.data.isSelected === false) {
  765. fontColor = '#ef4444';
  766. }
  767. return (
  768. <div style={{ marginLeft: level * 12 + 'px' }}>
  769. <div
  770. style={{
  771. padding: '6px 8px',
  772. borderRadius: '4px',
  773. cursor: 'pointer',
  774. background: 'transparent',
  775. border: isSelected ? '1px solid #3b82f6' : '1px solid transparent',
  776. display: 'flex',
  777. alignItems: 'center',
  778. gap: '6px',
  779. transition: 'all 0.2s ease',
  780. position: 'relative',
  781. overflow: 'visible',
  782. }}
  783. onMouseEnter={(e) => {
  784. if (!isSelected) e.currentTarget.style.background = '#f9fafb';
  785. }}
  786. onMouseLeave={(e) => {
  787. if (!isSelected) e.currentTarget.style.background = 'transparent';
  788. }}
  789. >
  790. {/* 策略类型竖线 */}
  791. <div style={{
  792. width: '3px',
  793. height: '20px',
  794. background: strategyColor,
  795. borderRadius: '2px',
  796. flexShrink: 0,
  797. position: 'relative',
  798. zIndex: 1,
  799. }} />
  800. {hasChildren && (
  801. <span
  802. style={{
  803. fontSize: '10px',
  804. color: '#6b7280',
  805. cursor: 'pointer',
  806. width: '16px',
  807. textAlign: 'center',
  808. position: 'relative',
  809. zIndex: 1,
  810. }}
  811. onClick={(e) => {
  812. e.stopPropagation();
  813. onToggle();
  814. }}
  815. >
  816. {isCollapsed ? '▶' : '▼'}
  817. </span>
  818. )}
  819. {!hasChildren && <span style={{ width: '16px', position: 'relative', zIndex: 1 }}></span>}
  820. <div
  821. style={{
  822. flex: 1,
  823. fontSize: '12px',
  824. color: '#374151',
  825. position: 'relative',
  826. zIndex: 1,
  827. minWidth: 0,
  828. display: 'flex',
  829. flexDirection: 'column',
  830. gap: '4px',
  831. }}
  832. onClick={onSelect}
  833. >
  834. <div style={{
  835. display: 'flex',
  836. alignItems: 'center',
  837. gap: '8px',
  838. }}>
  839. <div style={{
  840. fontWeight: level === 0 ? '600' : '400',
  841. maxWidth: '180px',
  842. flex: 1,
  843. minWidth: 0,
  844. color: node.data.scoreColor || fontColor,
  845. }}
  846. title={node.data.title || node.id}
  847. >
  848. {truncateMiddle(node.data.title || node.id, 18)}
  849. </div>
  850. {/* 分数显示 - 步骤和轮次节点不显示分数 */}
  851. {nodeActualType !== 'step' && nodeActualType !== 'round' && (
  852. <span style={{
  853. fontSize: '11px',
  854. color: '#6b7280',
  855. fontWeight: '500',
  856. flexShrink: 0,
  857. minWidth: '35px',
  858. textAlign: 'right',
  859. }}>
  860. {score.toFixed(2)}
  861. </span>
  862. )}
  863. </div>
  864. {/* 分数下划线 - 步骤和轮次节点不显示 */}
  865. {nodeActualType !== 'step' && nodeActualType !== 'round' && (
  866. <div style={{
  867. width: (score * 100) + '%',
  868. height: '2px',
  869. background: getScoreColor(score),
  870. borderRadius: '1px',
  871. }} />
  872. )}
  873. </div>
  874. </div>
  875. {hasChildren && !isCollapsed && (
  876. <div>
  877. {children}
  878. </div>
  879. )}
  880. </div>
  881. );
  882. }
  883. // 使用 dagre 自动布局
  884. function getLayoutedElements(nodes, edges, direction = 'LR') {
  885. console.log('🎯 Starting layout with dagre...');
  886. console.log('Input:', nodes.length, 'nodes,', edges.length, 'edges');
  887. // 检查 dagre 是否加载
  888. if (typeof window === 'undefined' || typeof window.dagre === 'undefined') {
  889. console.warn('⚠️ Dagre not loaded, using fallback layout');
  890. // 降级到简单布局
  891. const levelGroups = {};
  892. nodes.forEach(node => {
  893. const level = node.data.level || 0;
  894. if (!levelGroups[level]) levelGroups[level] = [];
  895. levelGroups[level].push(node);
  896. });
  897. Object.entries(levelGroups).forEach(([level, nodeList]) => {
  898. const x = parseInt(level) * 350;
  899. nodeList.forEach((node, index) => {
  900. node.position = { x, y: index * 150 };
  901. node.targetPosition = 'left';
  902. node.sourcePosition = 'right';
  903. });
  904. });
  905. return { nodes, edges };
  906. }
  907. try {
  908. const dagreGraph = new window.dagre.graphlib.Graph();
  909. dagreGraph.setDefaultEdgeLabel(() => ({}));
  910. const isHorizontal = direction === 'LR';
  911. dagreGraph.setGraph({
  912. rankdir: direction,
  913. nodesep: 120, // 垂直间距 - 增加以避免节点重叠
  914. ranksep: 280, // 水平间距 - 增加以容纳更宽的节点
  915. });
  916. // 添加节点 - 根据节点类型设置不同的尺寸
  917. nodes.forEach((node) => {
  918. let nodeWidth = 280;
  919. let nodeHeight = 180;
  920. // note 节点有轮播图,需要更大的空间
  921. if (node.type === 'note') {
  922. nodeWidth = 320;
  923. nodeHeight = 350; // 增加高度以容纳轮播图
  924. }
  925. dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  926. });
  927. // 添加边
  928. edges.forEach((edge) => {
  929. dagreGraph.setEdge(edge.source, edge.target);
  930. });
  931. // 计算布局
  932. window.dagre.layout(dagreGraph);
  933. console.log('✅ Dagre layout completed');
  934. // 更新节点位置和 handle 位置
  935. nodes.forEach((node) => {
  936. const nodeWithPosition = dagreGraph.node(node.id);
  937. if (!nodeWithPosition) {
  938. console.warn('Node position not found for:', node.id);
  939. return;
  940. }
  941. node.targetPosition = isHorizontal ? 'left' : 'top';
  942. node.sourcePosition = isHorizontal ? 'right' : 'bottom';
  943. // 根据节点类型获取尺寸
  944. let nodeWidth = 280;
  945. let nodeHeight = 180;
  946. if (node.type === 'note') {
  947. nodeWidth = 320;
  948. nodeHeight = 350;
  949. }
  950. // 将 dagre 的中心点位置转换为 React Flow 的左上角位置
  951. node.position = {
  952. x: nodeWithPosition.x - nodeWidth / 2,
  953. y: nodeWithPosition.y - nodeHeight / 2,
  954. };
  955. });
  956. console.log('✅ Layout completed, sample node:', nodes[0]);
  957. return { nodes, edges };
  958. } catch (error) {
  959. console.error('❌ Error in dagre layout:', error);
  960. console.error('Error details:', error.message, error.stack);
  961. // 降级处理
  962. console.log('Using fallback layout...');
  963. const levelGroups = {};
  964. nodes.forEach(node => {
  965. const level = node.data.level || 0;
  966. if (!levelGroups[level]) levelGroups[level] = [];
  967. levelGroups[level].push(node);
  968. });
  969. Object.entries(levelGroups).forEach(([level, nodeList]) => {
  970. const x = parseInt(level) * 350;
  971. nodeList.forEach((node, index) => {
  972. node.position = { x, y: index * 150 };
  973. node.targetPosition = 'left';
  974. node.sourcePosition = 'right';
  975. });
  976. });
  977. return { nodes, edges };
  978. }
  979. }
  980. function transformData(data) {
  981. const nodes = [];
  982. const edges = [];
  983. const originalIdToCanvasId = {}; // 原始ID -> 画布ID的映射
  984. const canvasIdToNodeData = {}; // 避免重复创建相同的节点
  985. // 创建节点
  986. Object.entries(data.nodes).forEach(([originalId, node]) => {
  987. // 统一处理所有类型的节点
  988. const nodeType = node.type || 'query';
  989. // 直接使用originalId作为canvasId,避免冲突
  990. const canvasId = originalId;
  991. originalIdToCanvasId[originalId] = canvasId;
  992. // 如果这个 canvasId 还没有创建过节点,则创建
  993. if (!canvasIdToNodeData[canvasId]) {
  994. canvasIdToNodeData[canvasId] = true;
  995. // 根据节点类型创建不同的数据结构
  996. if (nodeType === 'note' || nodeType === 'post') {
  997. nodes.push({
  998. id: canvasId,
  999. originalId: originalId,
  1000. type: 'note',
  1001. data: {
  1002. title: node.query || node.title || '帖子',
  1003. matchLevel: node.match_level,
  1004. score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00',
  1005. description: node.body_text || node.desc || '',
  1006. isSelected: node.is_selected !== undefined ? node.is_selected : true,
  1007. imageList: node.image_list || [],
  1008. noteUrl: node.note_url || '',
  1009. evaluationReason: node.evaluationReason || node.evaluation_reason || '',
  1010. interact_info: node.interact_info || {},
  1011. nodeType: nodeType,
  1012. },
  1013. position: { x: 0, y: 0 },
  1014. });
  1015. } else {
  1016. // query, seg, q, search, root 等节点
  1017. let displayTitle = node.query || originalId;
  1018. nodes.push({
  1019. id: canvasId,
  1020. originalId: originalId,
  1021. type: 'query', // 使用 query 组件渲染所有非note节点
  1022. data: {
  1023. title: displayTitle,
  1024. level: node.level || 0,
  1025. score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00',
  1026. strategy: node.strategy || '',
  1027. parent: node.parent_query || '',
  1028. isSelected: node.is_selected !== undefined ? node.is_selected : true,
  1029. evaluationReason: node.evaluationReason || node.evaluation_reason || '',
  1030. nodeType: nodeType, // 传递实际节点类型用于样式
  1031. searchCount: node.search_count, // search 节点特有
  1032. totalPosts: node.total_posts, // search 节点特有
  1033. selectedWord: node.selected_word || '', // 加词节点特有 - 显示选择的词
  1034. scoreColor: node.scoreColor || null, // SUG节点的颜色标识
  1035. parentQScore: node.parentQScore || 0, // 父Q得分(用于调试)
  1036. },
  1037. position: { x: 0, y: 0 },
  1038. });
  1039. }
  1040. }
  1041. });
  1042. // 创建边 - 使用虚线样式,映射到画布ID
  1043. data.edges.forEach((edge, index) => {
  1044. const edgeColors = {
  1045. '初始分词': '#10b981',
  1046. '调用sug': '#06b6d4',
  1047. '同义改写': '#f59e0b',
  1048. '加词': '#3b82f6',
  1049. '抽象改写': '#8b5cf6',
  1050. '基于部分匹配改进': '#ec4899',
  1051. '结果分支-抽象改写': '#a855f7',
  1052. '结果分支-同义改写': '#fb923c',
  1053. 'query_to_note': '#ec4899',
  1054. };
  1055. const color = edgeColors[edge.strategy] || edgeColors[edge.edge_type] || '#d1d5db';
  1056. const isNoteEdge = edge.edge_type === 'query_to_note';
  1057. edges.push({
  1058. id: \`edge-\${index}\`,
  1059. source: originalIdToCanvasId[edge.from], // 使用画布ID
  1060. target: originalIdToCanvasId[edge.to], // 使用画布ID
  1061. type: 'simplebezier', // 使用简单贝塞尔曲线
  1062. animated: isNoteEdge,
  1063. style: {
  1064. stroke: color,
  1065. strokeWidth: isNoteEdge ? 2.5 : 2,
  1066. strokeDasharray: isNoteEdge ? '5,5' : '8,4',
  1067. },
  1068. markerEnd: {
  1069. type: 'arrowclosed',
  1070. color: color,
  1071. width: 20,
  1072. height: 20,
  1073. },
  1074. });
  1075. });
  1076. // 使用 dagre 自动计算布局 - 从左到右
  1077. return getLayoutedElements(nodes, edges, 'LR');
  1078. }
  1079. function FlowContent() {
  1080. // 画布使用简化数据
  1081. const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
  1082. console.log('🔍 Transforming data for canvas...');
  1083. const result = transformData(data);
  1084. console.log('✅ Canvas data:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
  1085. return result;
  1086. }, []);
  1087. // 目录使用完整数据(如果存在)
  1088. const { nodes: fullNodes, edges: fullEdges } = useMemo(() => {
  1089. if (data.fullData) {
  1090. console.log('🔍 Transforming full data for tree directory...');
  1091. const result = transformData(data.fullData);
  1092. console.log('✅ Directory data:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
  1093. return result;
  1094. }
  1095. // 如果没有 fullData,使用简化数据
  1096. return { nodes: initialNodes, edges: initialEdges };
  1097. }, [initialNodes, initialEdges]);
  1098. // 初始化:找出所有有子节点的节点,默认折叠(画布节点)
  1099. const initialCollapsedNodes = useMemo(() => {
  1100. const nodesWithChildren = new Set();
  1101. initialEdges.forEach(edge => {
  1102. nodesWithChildren.add(edge.source);
  1103. });
  1104. // 排除根节点(level 0),让根节点默认展开
  1105. const rootNode = initialNodes.find(n => n.data.level === 0);
  1106. if (rootNode) {
  1107. nodesWithChildren.delete(rootNode.id);
  1108. }
  1109. return nodesWithChildren;
  1110. }, [initialNodes, initialEdges]);
  1111. // 树节点的折叠状态需要在树构建后初始化
  1112. const [collapsedNodes, setCollapsedNodes] = useState(() => initialCollapsedNodes);
  1113. const [collapsedTreeNodes, setCollapsedTreeNodes] = useState(new Set());
  1114. const [selectedNodeId, setSelectedNodeId] = useState(null);
  1115. const [hiddenNodes, setHiddenNodes] = useState(new Set()); // 用户手动隐藏的节点
  1116. const [focusMode, setFocusMode] = useState(false); // 全局聚焦模式,默认关闭
  1117. const [focusedNodeId, setFocusedNodeId] = useState(null); // 单独聚焦的节点ID
  1118. const [sidebarWidth, setSidebarWidth] = useState(400); // 左侧目录宽度
  1119. const [isResizing, setIsResizing] = useState(false); // 是否正在拖拽调整宽度
  1120. // 拖拽调整侧边栏宽度的处理逻辑
  1121. const handleMouseDown = useCallback(() => {
  1122. setIsResizing(true);
  1123. }, []);
  1124. useEffect(() => {
  1125. if (!isResizing) return;
  1126. const handleMouseMove = (e) => {
  1127. const newWidth = e.clientX;
  1128. // 限制宽度范围:300px - 700px
  1129. if (newWidth >= 300 && newWidth <= 700) {
  1130. setSidebarWidth(newWidth);
  1131. }
  1132. };
  1133. const handleMouseUp = () => {
  1134. setIsResizing(false);
  1135. };
  1136. document.addEventListener('mousemove', handleMouseMove);
  1137. document.addEventListener('mouseup', handleMouseUp);
  1138. return () => {
  1139. document.removeEventListener('mousemove', handleMouseMove);
  1140. document.removeEventListener('mouseup', handleMouseUp);
  1141. };
  1142. }, [isResizing]);
  1143. // 获取 React Flow 实例以控制画布
  1144. const { setCenter, fitView } = useReactFlow();
  1145. // 获取某个节点的所有后代节点ID
  1146. const getDescendants = useCallback((nodeId) => {
  1147. const descendants = new Set();
  1148. const queue = [nodeId];
  1149. while (queue.length > 0) {
  1150. const current = queue.shift();
  1151. initialEdges.forEach(edge => {
  1152. if (edge.source === current && !descendants.has(edge.target)) {
  1153. descendants.add(edge.target);
  1154. queue.push(edge.target);
  1155. }
  1156. });
  1157. }
  1158. return descendants;
  1159. }, [initialEdges]);
  1160. // 获取直接父节点
  1161. const getDirectParents = useCallback((nodeId) => {
  1162. const parents = [];
  1163. initialEdges.forEach(edge => {
  1164. if (edge.target === nodeId) {
  1165. parents.push(edge.source);
  1166. }
  1167. });
  1168. return parents;
  1169. }, [initialEdges]);
  1170. // 获取直接子节点
  1171. const getDirectChildren = useCallback((nodeId) => {
  1172. const children = [];
  1173. initialEdges.forEach(edge => {
  1174. if (edge.source === nodeId) {
  1175. children.push(edge.target);
  1176. }
  1177. });
  1178. return children;
  1179. }, [initialEdges]);
  1180. // 切换节点折叠状态
  1181. const toggleNodeCollapse = useCallback((nodeId) => {
  1182. setCollapsedNodes(prev => {
  1183. const newSet = new Set(prev);
  1184. const descendants = getDescendants(nodeId);
  1185. if (newSet.has(nodeId)) {
  1186. // 展开:移除此节点,但保持其他折叠的节点
  1187. newSet.delete(nodeId);
  1188. } else {
  1189. // 折叠:添加此节点
  1190. newSet.add(nodeId);
  1191. }
  1192. return newSet;
  1193. });
  1194. }, [getDescendants]);
  1195. // 过滤可见的节点和边,并重新计算布局
  1196. const { nodes, edges } = useMemo(() => {
  1197. const nodesToHide = new Set();
  1198. // 判断使用哪个节点ID进行聚焦:优先使用单独聚焦的节点,否则使用全局聚焦模式的选中节点
  1199. const effectiveFocusNodeId = focusedNodeId || (focusMode ? selectedNodeId : null);
  1200. // 聚焦模式:只显示聚焦节点、其父节点和直接子节点
  1201. if (effectiveFocusNodeId) {
  1202. const visibleInFocus = new Set([effectiveFocusNodeId]);
  1203. // 添加所有父节点
  1204. initialEdges.forEach(edge => {
  1205. if (edge.target === effectiveFocusNodeId) {
  1206. visibleInFocus.add(edge.source);
  1207. }
  1208. });
  1209. // 添加所有直接子节点
  1210. initialEdges.forEach(edge => {
  1211. if (edge.source === effectiveFocusNodeId) {
  1212. visibleInFocus.add(edge.target);
  1213. }
  1214. });
  1215. // 隐藏不在聚焦范围内的节点
  1216. initialNodes.forEach(node => {
  1217. if (!visibleInFocus.has(node.id)) {
  1218. nodesToHide.add(node.id);
  1219. }
  1220. });
  1221. } else {
  1222. // 非聚焦模式:使用原有的折叠逻辑
  1223. // 收集所有被折叠节点的后代
  1224. collapsedNodes.forEach(collapsedId => {
  1225. const descendants = getDescendants(collapsedId);
  1226. descendants.forEach(id => nodesToHide.add(id));
  1227. });
  1228. }
  1229. // 添加用户手动隐藏的节点
  1230. hiddenNodes.forEach(id => nodesToHide.add(id));
  1231. const visibleNodes = initialNodes
  1232. .filter(node => !nodesToHide.has(node.id))
  1233. .map(node => ({
  1234. ...node,
  1235. data: {
  1236. ...node.data,
  1237. isCollapsed: collapsedNodes.has(node.id),
  1238. hasChildren: initialEdges.some(e => e.source === node.id),
  1239. onToggleCollapse: () => toggleNodeCollapse(node.id),
  1240. onHideSelf: () => {
  1241. setHiddenNodes(prev => {
  1242. const newSet = new Set(prev);
  1243. newSet.add(node.id);
  1244. return newSet;
  1245. });
  1246. },
  1247. onFocus: () => {
  1248. // 切换聚焦状态
  1249. if (focusedNodeId === node.id) {
  1250. setFocusedNodeId(null); // 如果已经聚焦,则取消聚焦
  1251. } else {
  1252. // 先取消之前的聚焦,然后聚焦到当前节点
  1253. setFocusedNodeId(node.id);
  1254. // 延迟聚焦视图到该节点
  1255. setTimeout(() => {
  1256. fitView({
  1257. nodes: [{ id: node.id }],
  1258. duration: 800,
  1259. padding: 0.3,
  1260. });
  1261. }, 100);
  1262. }
  1263. },
  1264. isFocused: focusedNodeId === node.id,
  1265. isHighlighted: selectedNodeId === node.id,
  1266. }
  1267. }));
  1268. const visibleEdges = initialEdges.filter(
  1269. edge => !nodesToHide.has(edge.source) && !nodesToHide.has(edge.target)
  1270. );
  1271. // 重新计算布局 - 只对可见节点
  1272. if (typeof window !== 'undefined' && typeof window.dagre !== 'undefined') {
  1273. try {
  1274. const dagreGraph = new window.dagre.graphlib.Graph();
  1275. dagreGraph.setDefaultEdgeLabel(() => ({}));
  1276. dagreGraph.setGraph({
  1277. rankdir: 'LR',
  1278. nodesep: 120, // 垂直间距 - 增加以避免节点重叠
  1279. ranksep: 280, // 水平间距 - 增加以容纳更宽的节点
  1280. });
  1281. visibleNodes.forEach((node) => {
  1282. let nodeWidth = 280;
  1283. let nodeHeight = 180;
  1284. // note 节点有轮播图,需要更大的空间
  1285. if (node.type === 'note') {
  1286. nodeWidth = 320;
  1287. nodeHeight = 350;
  1288. }
  1289. dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  1290. });
  1291. visibleEdges.forEach((edge) => {
  1292. dagreGraph.setEdge(edge.source, edge.target);
  1293. });
  1294. window.dagre.layout(dagreGraph);
  1295. visibleNodes.forEach((node) => {
  1296. const nodeWithPosition = dagreGraph.node(node.id);
  1297. if (nodeWithPosition) {
  1298. // 根据节点类型获取对应的尺寸
  1299. let nodeWidth = 280;
  1300. let nodeHeight = 180;
  1301. if (node.type === 'note') {
  1302. nodeWidth = 320;
  1303. nodeHeight = 350;
  1304. }
  1305. node.position = {
  1306. x: nodeWithPosition.x - nodeWidth / 2,
  1307. y: nodeWithPosition.y - nodeHeight / 2,
  1308. };
  1309. node.targetPosition = 'left';
  1310. node.sourcePosition = 'right';
  1311. }
  1312. });
  1313. console.log('✅ Dynamic layout recalculated for', visibleNodes.length, 'visible nodes');
  1314. } catch (error) {
  1315. console.error('❌ Error in dynamic layout:', error);
  1316. }
  1317. }
  1318. return { nodes: visibleNodes, edges: visibleEdges };
  1319. }, [initialNodes, initialEdges, collapsedNodes, hiddenNodes, focusMode, focusedNodeId, getDescendants, toggleNodeCollapse, selectedNodeId]);
  1320. // 构建树形结构 - 允许一个节点有多个父节点
  1321. // 为目录构建树(使用完整数据)
  1322. const buildTree = useCallback(() => {
  1323. // 使用完整数据构建目录树
  1324. const nodeMap = new Map();
  1325. fullNodes.forEach(node => {
  1326. nodeMap.set(node.id, node);
  1327. });
  1328. // 为每个节点创建树节点的副本(允许多次出现)
  1329. const createTreeNode = (nodeId, pathKey) => {
  1330. const node = nodeMap.get(nodeId);
  1331. if (!node) return null;
  1332. return {
  1333. ...node,
  1334. treeKey: pathKey, // 唯一的树路径key,用于React key
  1335. children: []
  1336. };
  1337. };
  1338. // 构建父子关系映射:记录每个节点的所有父节点,去重边
  1339. const parentToChildren = new Map();
  1340. const childToParents = new Map();
  1341. fullEdges.forEach(edge => {
  1342. // 记录父->子关系(去重:同一个父节点到同一个子节点只记录一次)
  1343. if (!parentToChildren.has(edge.source)) {
  1344. parentToChildren.set(edge.source, []);
  1345. }
  1346. const children = parentToChildren.get(edge.source);
  1347. if (!children.includes(edge.target)) {
  1348. children.push(edge.target);
  1349. }
  1350. // 记录子->父关系(用于判断是否有多个父节点,也去重)
  1351. if (!childToParents.has(edge.target)) {
  1352. childToParents.set(edge.target, []);
  1353. }
  1354. const parents = childToParents.get(edge.target);
  1355. if (!parents.includes(edge.source)) {
  1356. parents.push(edge.source);
  1357. }
  1358. });
  1359. // 递归构建树
  1360. const buildSubtree = (nodeId, pathKey, visitedInPath) => {
  1361. // 避免循环引用:如果当前路径中已经访问过这个节点,跳过
  1362. if (visitedInPath.has(nodeId)) {
  1363. return null;
  1364. }
  1365. const treeNode = createTreeNode(nodeId, pathKey);
  1366. if (!treeNode) return null;
  1367. const newVisitedInPath = new Set(visitedInPath);
  1368. newVisitedInPath.add(nodeId);
  1369. const children = parentToChildren.get(nodeId) || [];
  1370. treeNode.children = children
  1371. .map((childId, index) => buildSubtree(childId, pathKey + '-' + childId + '-' + index, newVisitedInPath))
  1372. .filter(child => child !== null);
  1373. return treeNode;
  1374. };
  1375. // 找出所有根节点(没有入边的节点)
  1376. const hasParent = new Set();
  1377. fullEdges.forEach(edge => {
  1378. hasParent.add(edge.target);
  1379. });
  1380. const roots = [];
  1381. fullNodes.forEach((node, index) => {
  1382. if (!hasParent.has(node.id)) {
  1383. const treeNode = buildSubtree(node.id, 'root-' + node.id + '-' + index, new Set());
  1384. if (treeNode) roots.push(treeNode);
  1385. }
  1386. });
  1387. return roots;
  1388. }, [fullNodes, fullEdges]);
  1389. const treeRoots = useMemo(() => buildTree(), [buildTree]);
  1390. // 生成树形文本结构(使用完整数据)
  1391. const generateTreeText = useCallback(() => {
  1392. const lines = [];
  1393. // 递归生成树形文本
  1394. const traverse = (nodes, prefix = '', isLast = true, depth = 0) => {
  1395. nodes.forEach((node, index) => {
  1396. const isLastNode = index === nodes.length - 1;
  1397. const nodeData = fullNodes.find(n => n.id === node.id)?.data || {};
  1398. const nodeType = nodeData.nodeType || node.data?.nodeType || 'unknown';
  1399. const title = nodeData.title || node.data?.title || node.id;
  1400. // 优先从node.data获取score,然后从nodeData获取
  1401. let score = null;
  1402. if (node.data?.score !== undefined && node.data?.score !== null) {
  1403. score = node.data.score;
  1404. } else if (node.data?.relevance_score !== undefined && node.data?.relevance_score !== null) {
  1405. score = node.data.relevance_score;
  1406. } else if (nodeData.score !== undefined && nodeData.score !== null) {
  1407. score = nodeData.score;
  1408. } else if (nodeData.relevance_score !== undefined && nodeData.relevance_score !== null) {
  1409. score = nodeData.relevance_score;
  1410. }
  1411. const strategy = nodeData.strategy || node.data?.strategy || '';
  1412. // 构建当前行 - score可能是数字或字符串,step/round节点不显示分数
  1413. const connector = isLastNode ? '└─' : '├─';
  1414. let scoreText = '';
  1415. if (nodeType !== 'step' && nodeType !== 'round' && score !== null && score !== undefined) {
  1416. // score可能已经是字符串格式(如 "0.05"),也可能是数字
  1417. const scoreStr = typeof score === 'number' ? score.toFixed(2) : score;
  1418. scoreText = \` (分数: \${scoreStr})\`;
  1419. }
  1420. const strategyText = strategy ? \` [\${strategy}]\` : '';
  1421. lines.push(\`\${prefix}\${connector} \${title}\${scoreText}\${strategyText}\`);
  1422. // 递归处理子节点
  1423. if (node.children && node.children.length > 0) {
  1424. const childPrefix = prefix + (isLastNode ? ' ' : '│ ');
  1425. traverse(node.children, childPrefix, isLastNode, depth + 1);
  1426. }
  1427. });
  1428. };
  1429. // 添加标题
  1430. const rootNode = fullNodes.find(n => n.data?.level === 0);
  1431. if (rootNode) {
  1432. lines.push(\`📊 查询扩展树形结构\`);
  1433. lines.push(\`原始问题: \${rootNode.data.title || rootNode.data.query}\`);
  1434. lines.push('');
  1435. }
  1436. traverse(treeRoots);
  1437. return lines.join('\\n');
  1438. }, [treeRoots, fullNodes]);
  1439. // 复制树形结构到剪贴板
  1440. const copyTreeToClipboard = useCallback(async () => {
  1441. try {
  1442. const treeText = generateTreeText();
  1443. await navigator.clipboard.writeText(treeText);
  1444. alert('✅ 树形结构已复制到剪贴板!');
  1445. } catch (err) {
  1446. console.error('复制失败:', err);
  1447. alert('❌ 复制失败,请手动复制');
  1448. }
  1449. }, [generateTreeText]);
  1450. // 初始化树节点折叠状态
  1451. useEffect(() => {
  1452. const getAllTreeKeys = (nodes) => {
  1453. const keys = new Set();
  1454. const traverse = (node) => {
  1455. if (node.children && node.children.length > 0) {
  1456. // 排除根节点
  1457. if (node.data.level !== 0) {
  1458. keys.add(node.treeKey);
  1459. }
  1460. node.children.forEach(traverse);
  1461. }
  1462. };
  1463. nodes.forEach(traverse);
  1464. return keys;
  1465. };
  1466. setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
  1467. }, [treeRoots]);
  1468. // 映射完整节点ID到画布简化节点ID
  1469. const mapTreeNodeToCanvasNode = useCallback((treeNodeId) => {
  1470. // 如果是简化模式,需要映射
  1471. if (data.fullData) {
  1472. // 从完整数据中找到节点
  1473. const fullNode = fullNodes.find(n => n.id === treeNodeId);
  1474. if (!fullNode) return treeNodeId;
  1475. // 根据节点类型和文本找到画布上的简化节点
  1476. const nodeText = fullNode.data.title || fullNode.data.query;
  1477. const nodeType = fullNode.data.nodeType || fullNode.type;
  1478. // Query类节点:找 query_xxx
  1479. if (['q', 'seg', 'sug', 'add_word', 'query'].includes(nodeType)) {
  1480. const canvasNode = initialNodes.find(n =>
  1481. (n.data.title === nodeText || n.data.query === nodeText) &&
  1482. ['query'].includes(n.type)
  1483. );
  1484. return canvasNode ? canvasNode.id : treeNodeId;
  1485. }
  1486. // Post节点:按note_id查找
  1487. if (nodeType === 'post' || nodeType === 'note') {
  1488. const noteId = fullNode.data.note_id;
  1489. if (noteId) {
  1490. const canvasNode = initialNodes.find(n => n.data.note_id === noteId);
  1491. return canvasNode ? canvasNode.id : treeNodeId;
  1492. }
  1493. }
  1494. // 其他节点类型(Round/Step等):直接返回
  1495. return treeNodeId;
  1496. }
  1497. // 非简化模式,直接返回
  1498. return treeNodeId;
  1499. }, [data.fullData, fullNodes, initialNodes]);
  1500. const renderTree = useCallback((treeNodes, level = 0) => {
  1501. return treeNodes.map(node => {
  1502. // 使用 treeKey 来区分树中的不同实例
  1503. const isCollapsed = collapsedTreeNodes.has(node.treeKey);
  1504. const isSelected = selectedNodeId === node.id;
  1505. return (
  1506. <TreeNode
  1507. key={node.treeKey}
  1508. node={node}
  1509. level={level}
  1510. isCollapsed={isCollapsed}
  1511. isSelected={isSelected}
  1512. onToggle={() => {
  1513. setCollapsedTreeNodes(prev => {
  1514. const newSet = new Set(prev);
  1515. if (newSet.has(node.treeKey)) {
  1516. newSet.delete(node.treeKey);
  1517. } else {
  1518. newSet.add(node.treeKey);
  1519. }
  1520. return newSet;
  1521. });
  1522. }}
  1523. onSelect={() => {
  1524. // 将目录节点ID映射到画布节点ID
  1525. const treeNodeId = node.id;
  1526. const canvasNodeId = mapTreeNodeToCanvasNode(treeNodeId);
  1527. // 检查画布上是否存在这个节点
  1528. const canvasNodeExists = initialNodes.some(n => n.id === canvasNodeId);
  1529. if (!canvasNodeExists) {
  1530. console.warn(\`节点 \${canvasNodeId} 在画布上不存在(可能被简化了)\`);
  1531. return;
  1532. }
  1533. const nodeId = canvasNodeId;
  1534. // 展开所有祖先节点
  1535. const ancestorIds = [nodeId];
  1536. const findAncestors = (id) => {
  1537. initialEdges.forEach(edge => {
  1538. if (edge.target === id && !ancestorIds.includes(edge.source)) {
  1539. ancestorIds.push(edge.source);
  1540. findAncestors(edge.source);
  1541. }
  1542. });
  1543. };
  1544. findAncestors(nodeId);
  1545. // 如果节点或其祖先被隐藏,先恢复它们
  1546. setHiddenNodes(prev => {
  1547. const newSet = new Set(prev);
  1548. ancestorIds.forEach(id => newSet.delete(id));
  1549. return newSet;
  1550. });
  1551. setSelectedNodeId(nodeId);
  1552. // 获取选中节点的直接子节点
  1553. const childrenIds = [];
  1554. initialEdges.forEach(edge => {
  1555. if (edge.source === nodeId) {
  1556. childrenIds.push(edge.target);
  1557. }
  1558. });
  1559. setCollapsedNodes(prev => {
  1560. const newSet = new Set(prev);
  1561. // 展开所有祖先节点
  1562. ancestorIds.forEach(id => newSet.delete(id));
  1563. // 展开选中节点本身
  1564. newSet.delete(nodeId);
  1565. // 展开选中节点的直接子节点
  1566. childrenIds.forEach(id => newSet.delete(id));
  1567. return newSet;
  1568. });
  1569. // 延迟聚焦,等待节点展开和布局重新计算
  1570. setTimeout(() => {
  1571. fitView({
  1572. nodes: [{ id: nodeId }],
  1573. duration: 800,
  1574. padding: 0.3,
  1575. });
  1576. }, 300);
  1577. }}
  1578. >
  1579. {node.children && node.children.length > 0 && renderTree(node.children, level + 1)}
  1580. </TreeNode>
  1581. );
  1582. });
  1583. }, [collapsedTreeNodes, selectedNodeId, nodes, setCenter, initialEdges, setCollapsedNodes, fitView, mapTreeNodeToCanvasNode, initialNodes, setHiddenNodes]);
  1584. console.log('📊 Rendering with', nodes.length, 'visible nodes and', edges.length, 'visible edges');
  1585. if (nodes.length === 0) {
  1586. return (
  1587. <div style={{ padding: 50, color: 'red', fontSize: 20 }}>
  1588. ERROR: No nodes to display!
  1589. </div>
  1590. );
  1591. }
  1592. return (
  1593. <div style={{ width: '100vw', height: '100vh', background: '#f9fafb', display: 'flex', flexDirection: 'column' }}>
  1594. {/* 顶部面包屑导航栏 */}
  1595. <div style={{
  1596. minHeight: '48px',
  1597. maxHeight: '120px',
  1598. background: 'white',
  1599. borderBottom: '1px solid #e5e7eb',
  1600. display: 'flex',
  1601. alignItems: 'flex-start',
  1602. padding: '12px 24px',
  1603. zIndex: 1000,
  1604. boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
  1605. flexShrink: 0,
  1606. overflowY: 'auto',
  1607. }}>
  1608. <div style={{ width: '100%' }}>
  1609. {selectedNodeId ? (
  1610. <div style={{ fontSize: '12px', color: '#6b7280' }}>
  1611. {/* 面包屑导航 - 显示所有路径 */}
  1612. {(() => {
  1613. const selectedNode = nodes.find(n => n.id === selectedNodeId);
  1614. if (!selectedNode) return null;
  1615. // 找到所有从根节点到当前节点的路径
  1616. const findAllPaths = (targetId) => {
  1617. const paths = [];
  1618. const buildPath = (nodeId, currentPath) => {
  1619. const node = initialNodes.find(n => n.id === nodeId);
  1620. if (!node) return;
  1621. const newPath = [node, ...currentPath];
  1622. // 找到所有父节点
  1623. const parents = initialEdges.filter(e => e.target === nodeId).map(e => e.source);
  1624. if (parents.length === 0) {
  1625. // 到达根节点
  1626. paths.push(newPath);
  1627. } else {
  1628. // 递归处理所有父节点
  1629. parents.forEach(parentId => {
  1630. buildPath(parentId, newPath);
  1631. });
  1632. }
  1633. };
  1634. buildPath(targetId, []);
  1635. return paths;
  1636. };
  1637. const allPaths = findAllPaths(selectedNodeId);
  1638. // 去重:将路径转换为字符串进行比较
  1639. const uniquePaths = [];
  1640. const pathStrings = new Set();
  1641. allPaths.forEach(path => {
  1642. const pathString = path.map(n => n.id).join('->');
  1643. if (!pathStrings.has(pathString)) {
  1644. pathStrings.add(pathString);
  1645. uniquePaths.push(path);
  1646. }
  1647. });
  1648. return (
  1649. <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
  1650. {uniquePaths.map((path, pathIndex) => (
  1651. <div key={pathIndex} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
  1652. {pathIndex > 0 && <span style={{ color: '#d1d5db', marginRight: '4px' }}>或</span>}
  1653. {path.map((node, index) => {
  1654. // 获取节点的 score、strategy 和 isSelected
  1655. const nodeScore = node.data.score ? parseFloat(node.data.score) : 0;
  1656. const nodeStrategy = getPrimaryStrategy(node.data); // 使用智能提取函数
  1657. const strategyColor = getStrategyColor(nodeStrategy);
  1658. const nodeIsSelected = node.type === 'note' ? node.data.matchLevel !== 'unsatisfied' : node.data.isSelected !== false;
  1659. const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型
  1660. // 计算路径节点字体颜色:根据分数提升幅度判断
  1661. let pathFontColor = '#374151'; // 默认颜色
  1662. if (node.type === 'note') {
  1663. pathFontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
  1664. } else if (node.data.seed_score !== undefined) {
  1665. const parentScore = parseFloat(node.data.seed_score);
  1666. const gain = nodeScore - parentScore;
  1667. pathFontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
  1668. } else if (index > 0) {
  1669. const prevNode = path[index - 1];
  1670. const prevScore = prevNode.data.score ? parseFloat(prevNode.data.score) : 0;
  1671. const gain = nodeScore - prevScore;
  1672. pathFontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
  1673. } else if (node.data.isSelected === false) {
  1674. pathFontColor = '#ef4444';
  1675. }
  1676. return (
  1677. <React.Fragment key={node.id + '-' + index}>
  1678. <span
  1679. onClick={() => {
  1680. const nodeId = node.id;
  1681. // 找到所有祖先节点
  1682. const ancestorIds = [nodeId];
  1683. const findAncestors = (id) => {
  1684. initialEdges.forEach(edge => {
  1685. if (edge.target === id && !ancestorIds.includes(edge.source)) {
  1686. ancestorIds.push(edge.source);
  1687. findAncestors(edge.source);
  1688. }
  1689. });
  1690. };
  1691. findAncestors(nodeId);
  1692. // 如果节点或其祖先被隐藏,先恢复它们
  1693. setHiddenNodes(prev => {
  1694. const newSet = new Set(prev);
  1695. ancestorIds.forEach(id => newSet.delete(id));
  1696. return newSet;
  1697. });
  1698. // 展开目录树中到达该节点的路径
  1699. // 需要找到所有包含该节点的树路径的 treeKey,并展开它们的父节点
  1700. setCollapsedTreeNodes(prev => {
  1701. const newSet = new Set(prev);
  1702. // 清空所有折叠状态,让目录树完全展开到选中节点
  1703. // 这样可以确保选中节点在目录中可见
  1704. return new Set();
  1705. });
  1706. setSelectedNodeId(nodeId);
  1707. setTimeout(() => {
  1708. fitView({
  1709. nodes: [{ id: nodeId }],
  1710. duration: 800,
  1711. padding: 0.3,
  1712. });
  1713. }, 100);
  1714. }}
  1715. style={{
  1716. padding: '6px 8px',
  1717. borderRadius: '4px',
  1718. background: 'white',
  1719. border: index === path.length - 1 ? '2px solid #3b82f6' : '1px solid #d1d5db',
  1720. color: '#374151',
  1721. fontWeight: index === path.length - 1 ? '600' : '400',
  1722. width: '180px',
  1723. cursor: 'pointer',
  1724. transition: 'all 0.2s ease',
  1725. position: 'relative',
  1726. display: 'inline-flex',
  1727. flexDirection: 'column',
  1728. gap: '4px',
  1729. }}
  1730. onMouseEnter={(e) => {
  1731. e.currentTarget.style.opacity = '0.8';
  1732. }}
  1733. onMouseLeave={(e) => {
  1734. e.currentTarget.style.opacity = '1';
  1735. }}
  1736. title={\`\${node.data.title || node.id} (Score: \${nodeScore.toFixed(2)}, Strategy: \${nodeStrategy}, Selected: \${nodeIsSelected})\`}
  1737. >
  1738. {/* 上半部分:竖线 + 图标 + 文字 + 分数 */}
  1739. <div style={{
  1740. display: 'flex',
  1741. alignItems: 'center',
  1742. gap: '6px',
  1743. }}>
  1744. {/* 策略类型竖线 */}
  1745. <div style={{
  1746. width: '3px',
  1747. height: '16px',
  1748. background: strategyColor,
  1749. borderRadius: '2px',
  1750. flexShrink: 0,
  1751. }} />
  1752. {/* 节点文字 */}
  1753. <span style={{
  1754. flex: 1,
  1755. fontSize: '12px',
  1756. color: pathFontColor,
  1757. }}>
  1758. {truncateMiddle(node.data.title || node.id, 18)}
  1759. </span>
  1760. {/* 分数显示 - 步骤和轮次节点不显示分数 */}
  1761. {nodeActualType !== 'step' && nodeActualType !== 'round' && (
  1762. <span style={{
  1763. fontSize: '10px',
  1764. color: '#6b7280',
  1765. fontWeight: '500',
  1766. flexShrink: 0,
  1767. minWidth: '35px',
  1768. textAlign: 'right',
  1769. }}>
  1770. {nodeScore.toFixed(2)}
  1771. </span>
  1772. )}
  1773. </div>
  1774. {/* 分数下划线 - 步骤和轮次节点不显示 */}
  1775. {nodeActualType !== 'step' && nodeActualType !== 'round' && (
  1776. <div style={{
  1777. width: (nodeScore * 100) + '%',
  1778. height: '2px',
  1779. background: getScoreColor(nodeScore),
  1780. borderRadius: '1px',
  1781. marginLeft: '9px',
  1782. }} />
  1783. )}
  1784. </span>
  1785. {index < path.length - 1 && <span style={{ color: '#9ca3af' }}>›</span>}
  1786. </React.Fragment>
  1787. )})}
  1788. </div>
  1789. ))}
  1790. </div>
  1791. );
  1792. })()}
  1793. </div>
  1794. ) : (
  1795. <div style={{ fontSize: '13px', color: '#9ca3af', textAlign: 'center' }}>
  1796. 选择一个节点查看路径
  1797. </div>
  1798. )}
  1799. </div>
  1800. </div>
  1801. {/* 主内容区:目录 + 画布 */}
  1802. <div style={{
  1803. display: 'flex',
  1804. flex: 1,
  1805. overflow: 'hidden',
  1806. cursor: isResizing ? 'col-resize' : 'default',
  1807. userSelect: isResizing ? 'none' : 'auto',
  1808. }}>
  1809. {/* 左侧目录树 */}
  1810. <div style={{
  1811. width: \`\${sidebarWidth}px\`,
  1812. background: 'white',
  1813. borderRight: '1px solid #e5e7eb',
  1814. display: 'flex',
  1815. flexDirection: 'column',
  1816. flexShrink: 0,
  1817. }}>
  1818. <div style={{
  1819. padding: '12px 16px',
  1820. borderBottom: '1px solid #e5e7eb',
  1821. display: 'flex',
  1822. justifyContent: 'space-between',
  1823. alignItems: 'center',
  1824. }}>
  1825. <span style={{
  1826. fontWeight: '600',
  1827. fontSize: '14px',
  1828. color: '#111827',
  1829. }}>
  1830. 节点目录
  1831. </span>
  1832. <div style={{ display: 'flex', gap: '6px' }}>
  1833. <button
  1834. onClick={() => {
  1835. setCollapsedTreeNodes(new Set());
  1836. }}
  1837. style={{
  1838. fontSize: '11px',
  1839. padding: '4px 8px',
  1840. borderRadius: '4px',
  1841. border: '1px solid #d1d5db',
  1842. background: 'white',
  1843. color: '#6b7280',
  1844. cursor: 'pointer',
  1845. fontWeight: '500',
  1846. }}
  1847. title="展开全部节点"
  1848. >
  1849. 全部展开
  1850. </button>
  1851. <button
  1852. onClick={() => {
  1853. const getAllTreeKeys = (nodes) => {
  1854. const keys = new Set();
  1855. const traverse = (node) => {
  1856. if (node.children && node.children.length > 0) {
  1857. keys.add(node.treeKey);
  1858. node.children.forEach(traverse);
  1859. }
  1860. };
  1861. nodes.forEach(traverse);
  1862. return keys;
  1863. };
  1864. setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
  1865. }}
  1866. style={{
  1867. fontSize: '11px',
  1868. padding: '4px 8px',
  1869. borderRadius: '4px',
  1870. border: '1px solid #d1d5db',
  1871. background: 'white',
  1872. color: '#6b7280',
  1873. cursor: 'pointer',
  1874. fontWeight: '500',
  1875. }}
  1876. title="折叠全部节点"
  1877. >
  1878. 全部折叠
  1879. </button>
  1880. <button
  1881. onClick={copyTreeToClipboard}
  1882. style={{
  1883. fontSize: '11px',
  1884. padding: '4px 8px',
  1885. borderRadius: '4px',
  1886. border: '1px solid #3b82f6',
  1887. background: '#3b82f6',
  1888. color: 'white',
  1889. cursor: 'pointer',
  1890. fontWeight: '500',
  1891. transition: 'all 0.2s',
  1892. }}
  1893. onMouseEnter={(e) => e.currentTarget.style.background = '#2563eb'}
  1894. onMouseLeave={(e) => e.currentTarget.style.background = '#3b82f6'}
  1895. title="复制树形结构为文本格式"
  1896. >
  1897. 📋 复制树形结构
  1898. </button>
  1899. </div>
  1900. </div>
  1901. <div style={{
  1902. flex: 1,
  1903. overflowX: 'auto',
  1904. overflowY: 'auto',
  1905. padding: '8px',
  1906. }}>
  1907. <div style={{ minWidth: 'fit-content' }}>
  1908. {renderTree(treeRoots)}
  1909. </div>
  1910. </div>
  1911. </div>
  1912. {/* 可拖拽的分隔条 */}
  1913. <div
  1914. onMouseDown={handleMouseDown}
  1915. style={{
  1916. width: '4px',
  1917. cursor: 'col-resize',
  1918. background: isResizing ? '#3b82f6' : 'transparent',
  1919. transition: isResizing ? 'none' : 'background 0.2s',
  1920. flexShrink: 0,
  1921. position: 'relative',
  1922. }}
  1923. onMouseEnter={(e) => e.currentTarget.style.background = '#e5e7eb'}
  1924. onMouseLeave={(e) => {
  1925. if (!isResizing) e.currentTarget.style.background = 'transparent';
  1926. }}
  1927. >
  1928. {/* 拖拽提示线 */}
  1929. <div style={{
  1930. position: 'absolute',
  1931. top: '50%',
  1932. left: '50%',
  1933. transform: 'translate(-50%, -50%)',
  1934. width: '1px',
  1935. height: '40px',
  1936. background: '#9ca3af',
  1937. opacity: isResizing ? 1 : 0.3,
  1938. }} />
  1939. </div>
  1940. {/* 画布区域 */}
  1941. <div style={{ flex: 1, position: 'relative' }}>
  1942. {/* 右侧图例 */}
  1943. <div style={{
  1944. position: 'absolute',
  1945. top: '20px',
  1946. right: '20px',
  1947. background: 'white',
  1948. padding: '16px',
  1949. borderRadius: '12px',
  1950. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
  1951. zIndex: 1000,
  1952. maxWidth: '260px',
  1953. border: '1px solid #e5e7eb',
  1954. }}>
  1955. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
  1956. <h3 style={{ fontSize: '14px', fontWeight: '600', color: '#111827', margin: 0 }}>图例</h3>
  1957. <button
  1958. onClick={() => setFocusMode(!focusMode)}
  1959. style={{
  1960. fontSize: '11px',
  1961. padding: '4px 8px',
  1962. borderRadius: '4px',
  1963. border: '1px solid',
  1964. borderColor: focusMode ? '#3b82f6' : '#d1d5db',
  1965. background: focusMode ? '#3b82f6' : 'white',
  1966. color: focusMode ? 'white' : '#6b7280',
  1967. cursor: 'pointer',
  1968. fontWeight: '500',
  1969. }}
  1970. title={focusMode ? '关闭聚焦模式' : '开启聚焦模式'}
  1971. >
  1972. {focusMode ? '🎯 聚焦' : '📊 全图'}
  1973. </button>
  1974. </div>
  1975. <div style={{ fontSize: '12px' }}>
  1976. {/* 画布节点展开/折叠控制 */}
  1977. <div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #f3f4f6' }}>
  1978. <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>节点控制</div>
  1979. <div style={{ display: 'flex', gap: '6px' }}>
  1980. <button
  1981. onClick={() => {
  1982. setCollapsedNodes(new Set());
  1983. }}
  1984. style={{
  1985. fontSize: '11px',
  1986. padding: '4px 8px',
  1987. borderRadius: '4px',
  1988. border: '1px solid #d1d5db',
  1989. background: 'white',
  1990. color: '#6b7280',
  1991. cursor: 'pointer',
  1992. fontWeight: '500',
  1993. flex: 1,
  1994. }}
  1995. title="展开画布中所有节点的子节点"
  1996. >
  1997. 全部展开
  1998. </button>
  1999. <button
  2000. onClick={() => {
  2001. const allNodeIds = new Set(initialNodes.map(n => n.id));
  2002. setCollapsedNodes(allNodeIds);
  2003. }}
  2004. style={{
  2005. fontSize: '11px',
  2006. padding: '4px 8px',
  2007. borderRadius: '4px',
  2008. border: '1px solid #d1d5db',
  2009. background: 'white',
  2010. color: '#6b7280',
  2011. cursor: 'pointer',
  2012. fontWeight: '500',
  2013. flex: 1,
  2014. }}
  2015. title="折叠画布中所有节点的子节点"
  2016. >
  2017. 全部折叠
  2018. </button>
  2019. </div>
  2020. </div>
  2021. <div style={{ paddingTop: '12px', borderTop: '1px solid #f3f4f6' }}>
  2022. <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>策略类型</div>
  2023. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2024. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#10b981', opacity: 0.7 }}></div>
  2025. <span style={{ color: '#6b7280', fontSize: '11px' }}>初始分词</span>
  2026. </div>
  2027. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2028. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#06b6d4', opacity: 0.7 }}></div>
  2029. <span style={{ color: '#6b7280', fontSize: '11px' }}>调用sug</span>
  2030. </div>
  2031. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2032. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#f59e0b', opacity: 0.7 }}></div>
  2033. <span style={{ color: '#6b7280', fontSize: '11px' }}>同义改写</span>
  2034. </div>
  2035. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2036. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#3b82f6', opacity: 0.7 }}></div>
  2037. <span style={{ color: '#6b7280', fontSize: '11px' }}>加词</span>
  2038. </div>
  2039. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2040. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#8b5cf6', opacity: 0.7 }}></div>
  2041. <span style={{ color: '#6b7280', fontSize: '11px' }}>抽象改写</span>
  2042. </div>
  2043. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2044. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#ec4899', opacity: 0.7 }}></div>
  2045. <span style={{ color: '#6b7280', fontSize: '11px' }}>基于部分匹配改进</span>
  2046. </div>
  2047. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2048. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#a855f7', opacity: 0.7 }}></div>
  2049. <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-抽象改写</span>
  2050. </div>
  2051. <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
  2052. <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#fb923c', opacity: 0.7 }}></div>
  2053. <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-同义改写</span>
  2054. </div>
  2055. </div>
  2056. <div style={{
  2057. marginTop: '12px',
  2058. paddingTop: '12px',
  2059. borderTop: '1px solid #f3f4f6',
  2060. fontSize: '11px',
  2061. color: '#9ca3af',
  2062. lineHeight: '1.5',
  2063. }}>
  2064. 💡 点击节点左上角 × 隐藏节点
  2065. </div>
  2066. {/* 隐藏节点列表 - 在图例内部 */}
  2067. {hiddenNodes.size > 0 && (
  2068. <div style={{
  2069. marginTop: '12px',
  2070. paddingTop: '12px',
  2071. borderTop: '1px solid #f3f4f6',
  2072. }}>
  2073. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
  2074. <h4 style={{ fontSize: '12px', fontWeight: '600', color: '#111827' }}>已隐藏节点</h4>
  2075. <button
  2076. onClick={() => setHiddenNodes(new Set())}
  2077. style={{
  2078. fontSize: '10px',
  2079. color: '#3b82f6',
  2080. background: 'none',
  2081. border: 'none',
  2082. cursor: 'pointer',
  2083. textDecoration: 'underline',
  2084. }}
  2085. >
  2086. 全部恢复
  2087. </button>
  2088. </div>
  2089. <div style={{ fontSize: '12px', maxHeight: '200px', overflow: 'auto' }}>
  2090. {Array.from(hiddenNodes).map(nodeId => {
  2091. const node = initialNodes.find(n => n.id === nodeId);
  2092. if (!node) return null;
  2093. return (
  2094. <div
  2095. key={nodeId}
  2096. style={{
  2097. display: 'flex',
  2098. justifyContent: 'space-between',
  2099. alignItems: 'center',
  2100. padding: '6px 8px',
  2101. margin: '4px 0',
  2102. background: '#f9fafb',
  2103. borderRadius: '6px',
  2104. fontSize: '11px',
  2105. }}
  2106. >
  2107. <span
  2108. style={{
  2109. flex: 1,
  2110. overflow: 'hidden',
  2111. textOverflow: 'ellipsis',
  2112. whiteSpace: 'nowrap',
  2113. color: '#374151',
  2114. }}
  2115. title={node.data.title || nodeId}
  2116. >
  2117. {node.data.title || nodeId}
  2118. </span>
  2119. <button
  2120. onClick={() => {
  2121. setHiddenNodes(prev => {
  2122. const newSet = new Set(prev);
  2123. newSet.delete(nodeId);
  2124. return newSet;
  2125. });
  2126. }}
  2127. style={{
  2128. marginLeft: '8px',
  2129. fontSize: '10px',
  2130. color: '#10b981',
  2131. background: 'none',
  2132. border: 'none',
  2133. cursor: 'pointer',
  2134. flexShrink: 0,
  2135. }}
  2136. >
  2137. 恢复
  2138. </button>
  2139. </div>
  2140. );
  2141. })}
  2142. </div>
  2143. </div>
  2144. )}
  2145. </div>
  2146. </div>
  2147. {/* React Flow 画布 */}
  2148. <ReactFlow
  2149. nodes={nodes}
  2150. edges={edges}
  2151. nodeTypes={nodeTypes}
  2152. fitView
  2153. fitViewOptions={{ padding: 0.2, duration: 500 }}
  2154. minZoom={0.1}
  2155. maxZoom={1.5}
  2156. nodesDraggable={true}
  2157. nodesConnectable={false}
  2158. elementsSelectable={true}
  2159. defaultEdgeOptions={{
  2160. type: 'smoothstep',
  2161. }}
  2162. proOptions={{ hideAttribution: true }}
  2163. onNodeClick={(event, clickedNode) => {
  2164. setSelectedNodeId(clickedNode.id);
  2165. }}
  2166. >
  2167. <Controls style={{ bottom: '20px', left: 'auto', right: '20px' }} />
  2168. <Background variant="dots" gap={20} size={1} color="#e5e7eb" />
  2169. </ReactFlow>
  2170. </div>
  2171. </div>
  2172. </div>
  2173. );
  2174. }
  2175. function App() {
  2176. return (
  2177. <ReactFlowProvider>
  2178. <FlowContent />
  2179. </ReactFlowProvider>
  2180. );
  2181. }
  2182. const root = createRoot(document.getElementById('root'));
  2183. root.render(<App />);
  2184. `;
  2185. fs.writeFileSync(reactComponentPath, reactComponent);
  2186. // 使用 esbuild 打包
  2187. console.log('🎨 Building modern visualization...');
  2188. build({
  2189. entryPoints: [reactComponentPath],
  2190. bundle: true,
  2191. outfile: path.join(__dirname, 'bundle_v2.js'),
  2192. format: 'iife',
  2193. loader: {
  2194. '.css': 'css',
  2195. },
  2196. minify: false,
  2197. sourcemap: 'inline',
  2198. // 强制所有 React 引用指向同一个位置,避免多副本
  2199. alias: {
  2200. 'react': path.join(__dirname, 'node_modules/react'),
  2201. 'react-dom': path.join(__dirname, 'node_modules/react-dom'),
  2202. 'react/jsx-runtime': path.join(__dirname, 'node_modules/react/jsx-runtime'),
  2203. 'react/jsx-dev-runtime': path.join(__dirname, 'node_modules/react/jsx-dev-runtime'),
  2204. },
  2205. }).then(() => {
  2206. // 读取打包后的 JS
  2207. const bundleJs = fs.readFileSync(path.join(__dirname, 'bundle_v2.js'), 'utf-8');
  2208. // 读取 CSS
  2209. const cssPath = path.join(__dirname, 'node_modules/@xyflow/react/dist/style.css');
  2210. const css = fs.readFileSync(cssPath, 'utf-8');
  2211. // 生成最终 HTML
  2212. const html = `<!DOCTYPE html>
  2213. <html lang="zh-CN">
  2214. <head>
  2215. <meta charset="UTF-8">
  2216. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  2217. <title>查询图可视化</title>
  2218. <link rel="preconnect" href="https://fonts.googleapis.com">
  2219. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  2220. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
  2221. <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
  2222. <script>
  2223. // 过滤特定的 React 警告
  2224. const originalError = console.error;
  2225. console.error = (...args) => {
  2226. if (typeof args[0] === 'string' && args[0].includes('Each child in a list should have a unique "key" prop')) {
  2227. return;
  2228. }
  2229. originalError.apply(console, args);
  2230. };
  2231. </script>
  2232. <style>
  2233. * {
  2234. margin: 0;
  2235. padding: 0;
  2236. box-sizing: border-box;
  2237. }
  2238. body {
  2239. font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  2240. overflow: hidden;
  2241. -webkit-font-smoothing: antialiased;
  2242. -moz-osx-font-smoothing: grayscale;
  2243. }
  2244. #root {
  2245. width: 100vw;
  2246. height: 100vh;
  2247. }
  2248. ${css}
  2249. /* 自定义样式覆盖 */
  2250. .react-flow__edge-path {
  2251. stroke-linecap: round;
  2252. }
  2253. .react-flow__controls {
  2254. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  2255. border: 1px solid #e5e7eb;
  2256. border-radius: 8px;
  2257. }
  2258. .react-flow__controls-button {
  2259. border: none;
  2260. border-bottom: 1px solid #e5e7eb;
  2261. }
  2262. .react-flow__controls-button:hover {
  2263. background: #f9fafb;
  2264. }
  2265. </style>
  2266. </head>
  2267. <body>
  2268. <div id="root"></div>
  2269. <script>${bundleJs}</script>
  2270. </body>
  2271. </html>`;
  2272. // 写入输出文件
  2273. fs.writeFileSync(outputFile, html);
  2274. // 清理临时文件
  2275. fs.unlinkSync(reactComponentPath);
  2276. fs.unlinkSync(path.join(__dirname, 'bundle_v2.js'));
  2277. console.log('✅ Visualization generated: ' + outputFile);
  2278. console.log('📊 Nodes: ' + Object.keys(data.nodes).length);
  2279. console.log('🔗 Edges: ' + data.edges.length);
  2280. }).catch(error => {
  2281. console.error('❌ Build error:', error);
  2282. process.exit(1);
  2283. });