index.js 85 KB

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