| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const { build } = require('esbuild');
- // const { convertV8ToGraph } = require('./convert_v8_to_graph'); // 已废弃,使用v3版本
- const { convertV8ToGraphV2, convertV8ToGraphSimplified } = require('./convert_v8_to_graph_v3');
- // 读取命令行参数
- const args = process.argv.slice(2);
- if (args.length === 0) {
- console.error('Usage: node index.js <path-to-run_context.json> [output.html] [--simplified]');
- process.exit(1);
- }
- const inputFile = args[0];
- const outputFile = args[1] || 'query_graph_output.html';
- const useSimplified = args.includes('--simplified');
- // 读取输入数据
- const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
- // 检测数据格式并转换
- let data;
- if (inputData.rounds && inputData.o) {
- // v6.1.2.8 格式,需要转换
- console.log('✨ 检测到 v6.1.2.8 格式,正在转换为图结构...');
- // 尝试读取 search_results.json(兼容旧版本)
- let searchResults = null;
- const searchResultsPath = path.join(path.dirname(inputFile), 'search_results.json');
- if (fs.existsSync(searchResultsPath)) {
- console.log('📄 读取外部搜索结果数据(兼容模式)...');
- searchResults = JSON.parse(fs.readFileSync(searchResultsPath, 'utf-8'));
- } else {
- console.log('✅ 使用 run_context.json 中的内嵌搜索结果');
- }
- // 尝试读取 search_extract.json(多模态提取数据)
- let extractionData = null;
- const extractionPath = path.join(path.dirname(inputFile), 'search_extract.json');
- if (fs.existsSync(extractionPath)) {
- console.log('📸 读取多模态提取数据...');
- extractionData = JSON.parse(fs.readFileSync(extractionPath, 'utf-8'));
- } else {
- console.log('ℹ️ 未找到 search_extract.json,跳过多模态展示');
- }
- // 选择转换函数
- let graphData;
- let fullData = null; // 用于目录的完整数据
- if (useSimplified) {
- console.log('🎨 使用简化视图(合并query节点)');
- // 生成简化版用于画布
- graphData = convertV8ToGraphSimplified(inputData, searchResults, extractionData);
- // 生成完整版用于目录
- const fullGraphData = convertV8ToGraphV2(inputData, searchResults, extractionData);
- fullData = {
- nodes: fullGraphData.nodes,
- edges: fullGraphData.edges,
- iterations: fullGraphData.iterations
- };
- console.log(`✅ 简化版: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
- console.log(`📋 完整版(用于目录): ${Object.keys(fullData.nodes).length} 个节点`);
- } else {
- console.log('📊 使用详细视图(完整流程)');
- graphData = convertV8ToGraphV2(inputData, searchResults, extractionData);
- console.log(`✅ 转换完成: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
- }
- data = {
- nodes: graphData.nodes,
- edges: graphData.edges,
- iterations: graphData.iterations,
- fullData: fullData // 传递完整数据
- };
- } else if (inputData.nodes && inputData.edges) {
- // v6.1.2.5 格式,直接使用
- console.log('✨ 检测到 v6.1.2.5 格式,直接使用');
- data = inputData;
- } else {
- console.error('❌ 无法识别的数据格式');
- process.exit(1);
- }
- // 创建临时 React 组件文件
- const reactComponentPath = path.join(__dirname, 'temp_flow_component_v2.jsx');
- const reactComponent = `
- import React, { useState, useCallback, useMemo, useEffect } from 'react';
- import { createRoot } from 'react-dom/client';
- import {
- ReactFlow,
- Controls,
- Background,
- useNodesState,
- useEdgesState,
- Handle,
- Position,
- useReactFlow,
- ReactFlowProvider,
- } from '@xyflow/react';
- import '@xyflow/react/dist/style.css';
- const data = ${JSON.stringify(data, null, 2)};
- // 根据节点类型获取边框颜色
- function getNodeTypeColor(type) {
- const typeColors = {
- 'root': '#6b21a8', // 紫色 - 根节点
- 'round': '#7c3aed', // 深紫 - Round节点
- 'step': '#f59e0b', // 橙色 - 步骤节点
- 'seg': '#10b981', // 绿色 - 分词
- 'q': '#3b82f6', // 蓝色 - Query
- 'sug': '#06b6d4', // 青色 - Sug建议词
- 'seed': '#84cc16', // 黄绿 - Seed
- 'add_word': '#22c55e', // 绿色 - 加词生成
- 'search_word': '#8b5cf6', // 紫色 - 搜索词
- 'post': '#ec4899', // 粉色 - 帖子
- 'filtered_sug': '#14b8a6',// 青绿 - 筛选的sug
- 'next_q': '#2563eb', // 深蓝 - 下轮查询
- 'next_seed': '#65a30d', // 深黄绿 - 下轮种子
- 'search': '#8b5cf6', // 深紫 - 搜索(兼容旧版)
- 'operation': '#f59e0b', // 橙色 - 操作节点(兼容旧版)
- 'query': '#3b82f6', // 蓝色 - 查询(兼容旧版)
- 'note': '#ec4899', // 粉色 - 帖子(兼容旧版)
- };
- return typeColors[type] || '#9ca3af';
- }
- // 查询节点组件 - 卡片样式
- function QueryNode({ id, data, sourcePosition, targetPosition }) {
- // 所有节点默认展开
- const expanded = true;
- // 获取节点类型颜色
- const typeColor = getNodeTypeColor(data.nodeType || 'query');
- return (
- <div>
- <Handle
- type="target"
- position={targetPosition || Position.Left}
- style={{ background: typeColor, width: 8, height: 8 }}
- />
- <div
- style={{
- padding: '12px',
- borderRadius: '8px',
- border: data.isHighlighted ? \`3px solid \${typeColor}\` :
- data.isCollapsed ? \`2px solid \${typeColor}\` :
- data.isSelected === false ? '2px dashed #d1d5db' :
- \`2px solid \${typeColor}\`,
- background: data.isHighlighted ? '#eef2ff' :
- data.isSelected === false ? '#f9fafb' : 'white',
- minWidth: '200px',
- maxWidth: '280px',
- boxShadow: data.isHighlighted ? '0 0 0 4px rgba(102, 126, 234, 0.25), 0 4px 16px rgba(102, 126, 234, 0.4)' :
- data.isCollapsed ? '0 4px 12px rgba(102, 126, 234, 0.15)' :
- data.level === 0 ? '0 4px 12px rgba(139, 92, 246, 0.15)' : '0 2px 6px rgba(0, 0, 0, 0.06)',
- transition: 'all 0.3s ease',
- cursor: 'pointer',
- position: 'relative',
- opacity: data.isSelected === false ? 0.6 : 1,
- }}
- >
- {/* 折叠当前节点按钮 - 左边 */}
- <div
- style={{
- position: 'absolute',
- top: '6px',
- left: '6px',
- width: '20px',
- height: '20px',
- borderRadius: '50%',
- background: '#f59e0b',
- color: 'white',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '11px',
- fontWeight: 'bold',
- cursor: 'pointer',
- transition: 'all 0.2s ease',
- zIndex: 10,
- }}
- onClick={(e) => {
- e.stopPropagation();
- if (data.onHideSelf) {
- data.onHideSelf();
- }
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.background = '#d97706';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.background = '#f59e0b';
- }}
- title="隐藏当前节点"
- >
- ×
- </div>
- {/* 聚焦按钮 - 右上角 */}
- <div
- style={{
- position: 'absolute',
- top: '6px',
- right: '6px',
- width: '20px',
- height: '20px',
- borderRadius: '50%',
- background: data.isFocused ? '#10b981' : '#e5e7eb',
- color: data.isFocused ? 'white' : '#6b7280',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '11px',
- fontWeight: 'bold',
- cursor: 'pointer',
- transition: 'all 0.2s ease',
- zIndex: 10,
- }}
- onClick={(e) => {
- e.stopPropagation();
- if (data.onFocus) {
- data.onFocus();
- }
- }}
- onMouseEnter={(e) => {
- if (!data.isFocused) {
- e.currentTarget.style.background = '#d1d5db';
- }
- }}
- onMouseLeave={(e) => {
- if (!data.isFocused) {
- e.currentTarget.style.background = '#e5e7eb';
- }
- }}
- title={data.isFocused ? '取消聚焦' : '聚焦到此节点'}
- >
- 🎯
- </div>
- {/* 折叠/展开子节点按钮 - 右边第二个位置 */}
- {data.hasChildren && (
- <div
- style={{
- position: 'absolute',
- top: '6px',
- right: '30px',
- width: '20px',
- height: '20px',
- borderRadius: '50%',
- background: data.isCollapsed ? '#667eea' : '#e5e7eb',
- color: data.isCollapsed ? 'white' : '#6b7280',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '11px',
- fontWeight: 'bold',
- cursor: 'pointer',
- transition: 'all 0.2s ease',
- zIndex: 10,
- }}
- onClick={(e) => {
- e.stopPropagation();
- data.onToggleCollapse();
- }}
- title={data.isCollapsed ? '展开子节点' : '折叠子节点'}
- >
- {data.isCollapsed ? '+' : '−'}
- </div>
- )}
- {/* 卡片内容 */}
- <div>
- {/* 标题行 */}
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px', paddingLeft: '24px', paddingRight: data.hasChildren ? '54px' : '28px' }}>
- <div style={{ flex: 1 }}>
- <div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
- <div style={{
- fontSize: '13px',
- fontWeight: data.level === 0 ? '700' : '600',
- color: data.level === 0 ? '#6b21a8' : '#1f2937',
- lineHeight: '1.3',
- flex: 1,
- }}>
- {data.title}
- </div>
- {data.isSelected === false && (
- <div style={{
- fontSize: '9px',
- padding: '1px 4px',
- borderRadius: '3px',
- background: '#fee2e2',
- color: '#991b1b',
- fontWeight: '500',
- flexShrink: 0,
- }}>
- 未选中
- </div>
- )}
- </div>
- </div>
- </div>
- {/* 展开的详细信息 - 始终显示 */}
- <div style={{ fontSize: '11px', lineHeight: 1.4 }}>
- <div style={{ display: 'flex', gap: '4px', marginBottom: '6px', flexWrap: 'wrap' }}>
- <span style={{
- display: 'inline-block',
- padding: '1px 6px',
- borderRadius: '10px',
- background: '#eff6ff',
- color: '#3b82f6',
- fontSize: '10px',
- fontWeight: '500',
- }}>
- Lv.{data.level}
- </span>
- <span style={{
- display: 'inline-block',
- padding: '1px 6px',
- borderRadius: '10px',
- background: '#f0fdf4',
- color: '#16a34a',
- fontSize: '10px',
- fontWeight: '500',
- }}>
- {data.score}
- </span>
- {data.strategy && data.strategy !== 'root' && (
- <span style={{
- display: 'inline-block',
- padding: '1px 6px',
- borderRadius: '10px',
- background: '#fef3c7',
- color: '#92400e',
- fontSize: '10px',
- fontWeight: '500',
- }}>
- {data.strategy}
- </span>
- )}
- {(data.typeLabel || data.type_label) && (
- <span style={{
- display: 'inline-block',
- padding: '1px 6px',
- borderRadius: '10px',
- background: '#fce7f3',
- color: '#9f1239',
- fontSize: '10px',
- fontWeight: '500',
- }}>
- {data.typeLabel || data.type_label}
- </span>
- )}
- {data.is_suggestion && data.suggestion_label && (
- <span style={{
- display: 'inline-block',
- padding: '1px 6px',
- borderRadius: '10px',
- background: '#ede9fe',
- color: '#6d28d9',
- fontSize: '10px',
- fontWeight: '600',
- }}>
- {data.suggestion_label}
- </span>
- )}
- </div>
- {data.parent && (
- <div style={{ color: '#6b7280', fontSize: '10px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #f3f4f6' }}>
- <strong>Parent:</strong> {data.parent}
- </div>
- )}
- {data.nodeType === 'domain_combination' && Array.isArray(data.source_word_details) && data.source_word_details.length > 0 && (
- <div style={{
- marginTop: '6px',
- paddingTop: '6px',
- borderTop: '1px solid #f3f4f6',
- fontSize: '10px',
- color: '#6b7280',
- lineHeight: '1.5',
- }}>
- <strong style={{ color: '#4b5563' }}>来源词得分:</strong>
- <div style={{ marginTop: '4px', display: 'flex', flexDirection: 'column', gap: '4px' }}>
- {data.source_word_details.map((detail, idx) => {
- const words = (detail.words || []).map((w) => {
- const numericScore = typeof w.score === 'number' ? w.score : parseFloat(w.score || '0');
- const formattedScore = Number.isFinite(numericScore) ? numericScore.toFixed(2) : '0.00';
- return w.text + ' (' + formattedScore + ')';
- }).join(' + ');
- return (
- <div key={idx} style={{ display: 'flex', flexWrap: 'wrap', gap: '4px', alignItems: 'center' }}>
- <span style={{ color: '#2563eb' }}>{words}</span>
- </div>
- );
- })}
- </div>
- <div style={{ marginTop: '4px', fontWeight: '500', color: data.is_above_sources ? '#16a34a' : '#dc2626' }}>
- {data.is_above_sources ? '✅ 组合得分高于所有来源词' : '⚠️ 组合得分未超过全部来源词'}
- </div>
- </div>
- )}
- {data.selectedWord && (
- <div style={{
- marginTop: '6px',
- paddingTop: '6px',
- borderTop: '1px solid #f3f4f6',
- fontSize: '10px',
- color: '#6b7280',
- lineHeight: '1.5',
- }}>
- <strong style={{ color: '#4b5563' }}>选择词:</strong>
- <span style={{ marginLeft: '4px', color: '#3b82f6', fontWeight: '500' }}>{data.selectedWord}</span>
- {data.seed_score !== undefined && (
- <div style={{ marginTop: '4px' }}>
- <strong style={{ color: '#4b5563' }}>种子得分:</strong>
- <span style={{ marginLeft: '4px', color: '#16a34a', fontWeight: '500' }}>
- {typeof data.seed_score === 'number' ? data.seed_score.toFixed(2) : data.seed_score}
- </span>
- </div>
- )}
- </div>
- )}
- {data.evaluationReason && (
- <div style={{
- marginTop: '6px',
- paddingTop: '6px',
- borderTop: '1px solid #f3f4f6',
- fontSize: '10px',
- color: '#6b7280',
- lineHeight: '1.5',
- }}>
- <strong style={{ color: '#4b5563' }}>评估:</strong>
- <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
- </div>
- )}
- {data.occurrences && data.occurrences.length > 1 && (
- <div style={{
- marginTop: '6px',
- paddingTop: '6px',
- borderTop: '1px solid #f3f4f6',
- fontSize: '10px',
- color: '#6b7280',
- }}>
- <strong style={{ color: '#4b5563' }}>演化历史 ({data.occurrences.length}次):</strong>
- <div style={{ marginTop: '4px' }}>
- {data.occurrences.map((occ, idx) => (
- <div key={idx} style={{ marginTop: '2px', paddingLeft: '8px' }}>
- <span style={{ color: '#3b82f6', fontWeight: '500' }}>R{occ.round}</span>
- {' · '}
- <span>{occ.strategy}</span>
- {occ.score !== undefined && (
- <span style={{ color: '#16a34a', marginLeft: '4px' }}>
- ({typeof occ.score === 'number' ? occ.score.toFixed(2) : occ.score})
- </span>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
- {data.hasSearchResults && (
- <div style={{
- marginTop: '6px',
- paddingTop: '6px',
- borderTop: '1px solid #f3f4f6',
- fontSize: '10px',
- background: '#fef3c7',
- padding: '4px 6px',
- borderRadius: '4px',
- color: '#92400e',
- fontWeight: '500',
- }}>
- 🔍 找到 {data.postCount} 个帖子
- </div>
- )}
- </div>
- </div>
- </div>
- <Handle
- type="source"
- position={sourcePosition || Position.Right}
- style={{ background: '#667eea', width: 8, height: 8 }}
- />
- </div>
- );
- }
- // 笔记节点组件 - 卡片样式,带轮播图
- function NoteNode({ id, data, sourcePosition, targetPosition }) {
- const [currentImageIndex, setCurrentImageIndex] = useState(0);
- const [showEvalDetails, setShowEvalDetails] = useState(false);
- const expanded = true;
- const hasImages = data.imageList && data.imageList.length > 0;
- const nextImage = (e) => {
- e.stopPropagation();
- if (hasImages) {
- setCurrentImageIndex((prev) => (prev + 1) % data.imageList.length);
- }
- };
- const prevImage = (e) => {
- e.stopPropagation();
- if (hasImages) {
- setCurrentImageIndex((prev) => (prev - 1 + data.imageList.length) % data.imageList.length);
- }
- };
- const handleCardClick = (e) => {
- // 如果点击的是链接或按钮(或其子元素),不处理(避免双重触发)
- if (e.target.closest('a') || e.target.closest('button')) {
- return;
- }
- // 打开原帖链接
- if (data.note_url) {
- window.open(data.note_url, '_blank', 'noopener,noreferrer');
- }
- };
- return (
- <div>
- <Handle
- type="target"
- position={targetPosition || Position.Left}
- style={{ background: '#ec4899', width: 8, height: 8 }}
- />
- <div
- onClick={handleCardClick}
- style={{
- padding: '28px',
- borderRadius: '40px',
- border: data.isHighlighted ? '6px solid #ec4899' : '4px solid #fce7f3',
- background: data.isHighlighted ? '#eef2ff' : 'white',
- minWidth: '880px',
- maxWidth: '1200px',
- boxShadow: data.isHighlighted ? '0 0 0 8px rgba(236, 72, 153, 0.25), 0 8px 32px rgba(236, 72, 153, 0.4)' : '0 8px 24px rgba(236, 72, 153, 0.15)',
- transition: 'all 0.3s ease',
- cursor: 'pointer',
- }}
- >
- {/* 🆕 原始问题展示 - 最顶部 */}
- {data.originalQuestion && (
- <div style={{
- marginBottom: '20px',
- paddingBottom: '20px',
- borderBottom: '2px solid #fce7f3',
- }}>
- <div style={{
- fontSize: '36px',
- color: '#6b21a8',
- lineHeight: '1.4',
- fontWeight: '600',
- }}>
- <span style={{ fontWeight: '700' }}>[原始需求问题]</span> {data.originalQuestion}
- </div>
- </div>
- )}
- {/* 帖子标题 - 明确标注 */}
- <div style={{ marginBottom: '20px', paddingBottom: '16px', borderBottom: '2px solid #fce7f3' }}>
- <div style={{ fontSize: '38px', fontWeight: '600', color: '#831843', lineHeight: '1.4' }}>
- <span style={{ fontSize: '32px', color: '#831843', fontWeight: '500' }}>帖子标题: </span>
- {data.title.replace(/^\[R\]\s*/, '')}
- </div>
- </div>
- {/* V3评估信息 - 可展开 */}
- {data.evaluator_version === 'v3.0' && (
- <div style={{ marginBottom: '20px', paddingBottom: '16px', borderBottom: '2px solid #fce7f3' }}>
- {/* 第1行:知识判定 + 内容知识 + 星级 */}
- <div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '10px', flexWrap: 'wrap' }}>
- <span style={{ fontSize: '28px', fontWeight: '600', color: data.is_knowledge ? '#166534' : '#991b1b' }}>
- {data.is_knowledge ? '✓ 是知识' : '✗ 非知识'}
- </span>
- {data.is_content_knowledge !== null && data.is_content_knowledge !== undefined && (
- <>
- <span style={{ fontSize: '28px', fontWeight: '600', color: data.is_content_knowledge ? '#166534' : '#991b1b' }}>
- {data.is_content_knowledge ? '✓ 是内容知识' : '✗ 非内容知识'}
- </span>
- {data.is_content_knowledge && data.content_knowledge_evaluation?.knowledge_score != null && (
- <span style={{ fontSize: '24px', lineHeight: '1' }}>
- {'⭐'.repeat(Math.min(5, Math.ceil(data.content_knowledge_evaluation.knowledge_score / 20)))}
- </span>
- )}
- {data.is_content_knowledge && data.content_knowledge_evaluation?.knowledge_score != null && (
- <span style={{ fontSize: '26px', fontWeight: '600', color: '#166534' }}>
- {data.content_knowledge_evaluation.knowledge_score}分
- </span>
- )}
- </>
- )}
- </div>
- {/* 第2行:匹配度得分(仅内容知识显示) */}
- {data.is_content_knowledge && data.final_score !== null && data.final_score !== undefined && (
- <div style={{ display: 'flex', alignItems: 'center', gap: '16px', flexWrap: 'wrap', marginBottom: '12px' }}>
- <span style={{ fontSize: '32px', fontWeight: '700', color: data.final_score >= 60 ? '#166534' : '#ea580c' }}>
- 匹配度得分 {data.final_score.toFixed(1)}分
- </span>
- <span style={{
- padding: '4px 16px',
- borderRadius: '20px',
- fontSize: '26px',
- fontWeight: '600',
- background: data.final_score >= 85 ? '#dcfce7' : data.final_score >= 60 ? '#fef3c7' : '#fee2e2',
- color: data.final_score >= 85 ? '#166534' : data.final_score >= 60 ? '#854d0e' : '#991b1b'
- }}>
- {data.match_level}
- </span>
- {data.purpose_score != null && (
- <span style={{ fontSize: '26px', color: '#9f1239' }}>
- 目的{data.purpose_score}分
- </span>
- )}
- {data.category_score != null && (
- <span style={{ fontSize: '26px', color: '#9f1239' }}>
- 品类{data.category_score}分
- </span>
- )}
- </div>
- )}
- {/* 展开按钮(所有V3评估都显示) */}
- {data.evaluator_version === 'v3.0' && (
- <div style={{ marginBottom: '12px' }}>
- <button
- onClick={(e) => { e.stopPropagation(); setShowEvalDetails(!showEvalDetails); }}
- style={{
- fontSize: '24px',
- padding: '6px 16px',
- borderRadius: '12px',
- border: '2px solid #ec4899',
- background: 'white',
- color: '#ec4899',
- cursor: 'pointer',
- fontWeight: '600',
- transition: 'all 0.2s'
- }}
- >
- {showEvalDetails ? '收起详情 ▲' : '展开详情 ▼'}
- </button>
- </div>
- )}
- {/* 详细内容(展开后显示) */}
- {showEvalDetails && (
- <div style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid #f3f4f6' }}>
- {/* 1. 知识评估 */}
- {data.is_knowledge !== null && (
- <div style={{ marginBottom: '16px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
- <div style={{ fontSize: '36px', fontWeight: '600', color: '#831843', marginBottom: '8px' }}>
- 1️⃣ 知识评估
- </div>
- <div style={{ fontSize: '34px', color: '#9f1239', lineHeight: '1.4' }}>
- {data.knowledge_evaluation?.conclusion || '无评估信息'}
- </div>
- </div>
- )}
- {/* 2. 内容知识评估 */}
- {data.is_content_knowledge && data.content_knowledge_evaluation && (
- <div style={{ marginBottom: '16px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
- <div style={{ fontSize: '36px', fontWeight: '600', color: '#831843', marginBottom: '8px' }}>
- 2️⃣ 内容知识评估 ({data.knowledge_score || 0}分)
- </div>
- <div style={{ fontSize: '34px', color: '#9f1239', lineHeight: '1.4' }}>
- {data.content_knowledge_evaluation.summary || '无评估信息'}
- </div>
- </div>
- )}
- {/* 3. 与原始需求匹配 */}
- {(data.purpose_evaluation || data.category_evaluation) && (
- <div style={{ marginBottom: '16px', padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
- <div style={{ fontSize: '36px', fontWeight: '600', color: '#831843', marginBottom: '8px' }}>
- 3️⃣ 与原始需求匹配
- </div>
- {data.purpose_evaluation && (
- <div style={{ fontSize: '34px', color: '#9f1239', lineHeight: '1.4', marginBottom: '12px' }}>
- <div style={{ fontWeight: '600', marginBottom: '6px' }}>
- 目的性匹配({data.purpose_score}分)
- </div>
- <div>{data.purpose_evaluation.core_basis || '无评估信息'}</div>
- </div>
- )}
- {data.category_evaluation && (
- <div style={{ fontSize: '34px', color: '#9f1239', lineHeight: '1.4' }}>
- <div style={{ fontWeight: '600', marginBottom: '6px' }}>
- 品类匹配({data.category_score}分)
- </div>
- <div>{data.category_evaluation.core_basis || '无评估信息'}</div>
- </div>
- )}
- </div>
- )}
- </div>
- )}
- </div>
- )}
- {/* V2评估信息 - 兼容旧数据 */}
- {data.evaluator_version !== 'v3.0' && (data.knowledge_score !== undefined || data.post_relevance_score !== undefined || data.is_knowledge !== undefined) && (
- <div style={{
- marginBottom: '20px',
- paddingBottom: '16px',
- borderBottom: '2px solid #fce7f3',
- }}>
- {/* 知识评估 (V2) */}
- {(data.knowledge_score !== undefined || data.is_knowledge !== undefined) && (
- <div style={{ marginBottom: '16px' }}>
- <div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '8px' }}>
- {data.knowledge_level && (
- <span style={{ fontSize: '24px', lineHeight: '1' }}>
- {'⭐'.repeat(data.knowledge_level)}
- </span>
- )}
- {data.knowledge_score != null && (
- <span style={{
- fontSize: '34px',
- fontWeight: '700',
- color: data.knowledge_score >= 70 ? '#166534' : data.knowledge_score >= 40 ? '#854d0e' : '#991b1b',
- }}>
- 知识: {data.knowledge_score.toFixed(0)}分
- </span>
- )}
- {!data.knowledge_score && data.is_knowledge !== undefined && (
- <span style={{
- display: 'inline-block',
- padding: '6px 20px',
- borderRadius: '24px',
- fontSize: '34px',
- fontWeight: '600',
- background: data.is_knowledge ? '#dcfce7' : '#fee2e2',
- color: data.is_knowledge ? '#166534' : '#991b1b',
- }}>
- {data.is_knowledge ? '✓ 知识' : '✗ 非知识'}
- </span>
- )}
- </div>
- {data.knowledge_evaluation?.summary && (
- <div style={{ fontSize: '30px', color: '#9f1239', lineHeight: '1.4', marginTop: '8px' }}>
- {data.knowledge_evaluation.summary}
- </div>
- )}
- {!data.knowledge_evaluation?.summary && data.knowledge_reason && (
- <div style={{ fontSize: '30px', color: '#9f1239', lineHeight: '1.4', marginTop: '8px' }}>
- {data.knowledge_reason}
- </div>
- )}
- </div>
- )}
- {/* 相关性评估 (V2) */}
- {data.post_relevance_score != null && (
- <div>
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
- <span style={{ fontSize: '34px', fontWeight: '600', color: '#9f1239' }}>
- 相关性: {data.post_relevance_score.toFixed(0)}分
- </span>
- {data.relevance_conclusion && (
- <span style={{
- padding: '4px 16px',
- borderRadius: '20px',
- fontSize: '30px',
- fontWeight: '600',
- background: data.relevance_conclusion.includes('高度') ? '#dcfce7' : data.relevance_conclusion.includes('中度') ? '#fef3c7' : '#fee2e2',
- color: data.relevance_conclusion.includes('高度') ? '#166534' : data.relevance_conclusion.includes('中度') ? '#854d0e' : '#991b1b',
- }}>
- {data.relevance_conclusion}
- </span>
- )}
- </div>
- {data.relevance_evaluation?.summary && (
- <div style={{ fontSize: '30px', color: '#9f1239', lineHeight: '1.4' }}>
- {data.relevance_evaluation.summary}
- </div>
- )}
- {data.relevance_evaluation?.purpose_score != null && data.relevance_evaluation?.category_score != null && (
- <div style={{ fontSize: '28px', color: '#9f1239', marginTop: '6px', opacity: 0.8 }}>
- 目的性:{data.relevance_evaluation.purpose_score.toFixed(0)}分(70%) |
- 品类:{data.relevance_evaluation.category_score.toFixed(0)}分(30%)
- </div>
- )}
- </div>
- )}
- </div>
- )}
- {/* 轮播图 */}
- {hasImages && (
- <div style={{
- position: 'relative',
- marginBottom: '16px',
- borderRadius: '24px',
- overflow: 'hidden',
- }}>
- <img
- src={data.imageList[currentImageIndex].image_url}
- alt={\`Image \${currentImageIndex + 1}\`}
- style={{
- width: '100%',
- height: 'auto',
- objectFit: 'contain',
- display: 'block',
- }}
- onError={(e) => {
- e.target.style.display = 'none';
- }}
- />
- {data.imageList.length > 1 && (
- <>
- {/* 左右切换按钮 */}
- <button
- onClick={prevImage}
- style={{
- position: 'absolute',
- left: '8px',
- top: '50%',
- transform: 'translateY(-50%)',
- background: 'rgba(0, 0, 0, 0.5)',
- color: 'white',
- border: 'none',
- borderRadius: '50%',
- width: '48px',
- height: '48px',
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '28px',
- }}
- >
- ‹
- </button>
- <button
- onClick={nextImage}
- style={{
- position: 'absolute',
- right: '8px',
- top: '50%',
- transform: 'translateY(-50%)',
- background: 'rgba(0, 0, 0, 0.5)',
- color: 'white',
- border: 'none',
- borderRadius: '50%',
- width: '48px',
- height: '48px',
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- fontSize: '28px',
- }}
- >
- ›
- </button>
- {/* 图片计数 */}
- <div style={{
- position: 'absolute',
- bottom: '8px',
- right: '8px',
- background: 'rgba(0, 0, 0, 0.6)',
- color: 'white',
- padding: '4px 12px',
- borderRadius: '20px',
- fontSize: '20px',
- }}>
- {currentImageIndex + 1}/{data.imageList.length}
- </div>
- </>
- )}
- </div>
- )}
- {/* 互动数据 */}
- {data.interact_info && (
- <div style={{
- display: 'flex',
- gap: '16px',
- marginBottom: '16px',
- flexWrap: 'wrap',
- fontSize: '22px',
- color: '#9f1239',
- }}>
- {data.interact_info.liked_count > 0 && (
- <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
- ❤️ {data.interact_info.liked_count}
- </span>
- )}
- {data.interact_info.collected_count > 0 && (
- <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
- ⭐ {data.interact_info.collected_count}
- </span>
- )}
- {data.interact_info.comment_count > 0 && (
- <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
- 💬 {data.interact_info.comment_count}
- </span>
- )}
- {data.interact_info.shared_count > 0 && (
- <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
- 🔗 {data.interact_info.shared_count}
- </span>
- )}
- </div>
- )}
- {/* 被哪些query找到 */}
- {data.foundByQueries && data.foundByQueries.length > 0 && (
- <div style={{
- marginBottom: '16px',
- padding: '12px 16px',
- background: '#f0fdf4',
- borderRadius: '12px',
- fontSize: '20px',
- }}>
- <strong style={{ color: '#16a34a' }}>🔍 被找到:</strong>
- <div style={{ marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
- {data.foundByQueries.map((query, idx) => (
- <span key={idx} style={{
- display: 'inline-block',
- padding: '4px 12px',
- background: '#dcfce7',
- color: '#166534',
- borderRadius: '8px',
- fontSize: '18px',
- }}>
- {query}
- </span>
- ))}
- </div>
- {data.foundInRounds && data.foundInRounds.length > 0 && (
- <div style={{ marginTop: '8px', color: '#6b7280' }}>
- 出现在: Round {data.foundInRounds.join(', ')}
- </div>
- )}
- </div>
- )}
- {/* 标签 */}
- {(data.matchLevel || data.score) && (
- <div style={{ display: 'flex', gap: '12px', marginBottom: '16px', flexWrap: 'wrap' }}>
- {data.matchLevel && (
- <span style={{
- display: 'inline-block',
- padding: '4px 16px',
- borderRadius: '24px',
- background: '#fff1f2',
- color: '#be123c',
- fontSize: '20px',
- fontWeight: '500',
- }}>
- {data.matchLevel}
- </span>
- )}
- {/* Score标签已隐藏 - V2不再需要 */}
- </div>
- )}
- {/* 描述 */}
- {expanded && data.description && (
- <div style={{
- fontSize: '22px',
- color: '#9f1239',
- lineHeight: '1.5',
- paddingTop: '16px',
- borderTop: '2px solid #fbcfe8',
- }}>
- {data.description}
- </div>
- )}
- {/* 评估理由 */}
- {expanded && data.evaluationReason && (
- <div style={{
- fontSize: '20px',
- color: '#831843',
- lineHeight: '1.5',
- paddingTop: '16px',
- marginTop: '16px',
- borderTop: '2px solid #fbcfe8',
- }}>
- <strong style={{ color: '#9f1239' }}>评估:</strong>
- <div style={{ marginTop: '4px' }}>{data.evaluationReason}</div>
- </div>
- )}
- </div>
- <Handle
- type="source"
- position={sourcePosition || Position.Right}
- style={{ background: '#ec4899', width: 8, height: 8 }}
- />
- </div>
- );
- }
- // AnalysisNode 组件:展示AI分析(左侧OCR文字,右侧缩略图+描述)
- function AnalysisNode({ data }) {
- const nodeStyle = {
- background: '#fffbeb',
- border: '2px solid #fbbf24',
- borderRadius: '8px',
- padding: '12px',
- minWidth: '700px',
- maxWidth: '900px',
- fontSize: '12px',
- boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
- cursor: 'pointer',
- };
- const handleCardClick = (e) => {
- // 如果点击的是链接或按钮(或其子元素),不处理(避免双重触发)
- if (e.target.closest('a') || e.target.closest('button')) {
- return;
- }
- // 打开原帖链接
- if (data.note_url) {
- window.open(data.note_url, '_blank', 'noopener,noreferrer');
- }
- };
- return (
- <div style={nodeStyle} onClick={handleCardClick}>
- <Handle
- type="target"
- position={Position.Left}
- style={{ background: '#fbbf24', width: 8, height: 8 }}
- />
- {/* 标题 */}
- <div style={{
- fontSize: '14px',
- fontWeight: 'bold',
- marginBottom: '8px',
- color: '#92400e',
- }}>
- 🖼️ {data.query}
- </div>
- {/* 评分和互动数据 */}
- <div style={{
- display: 'flex',
- justifyContent: 'space-between',
- marginBottom: '8px',
- padding: '6px',
- background: '#fef3c7',
- borderRadius: '4px',
- }}>
- <div style={{ fontSize: '11px', fontWeight: 'bold' }}>
- Score: {data.interact_info?.relevance_score || 0}
- </div>
- <div style={{ display: 'flex', gap: '12px', fontSize: '11px' }}>
- {data.interact_info?.liked_count > 0 && (
- <span>❤️ {data.interact_info.liked_count}</span>
- )}
- {data.interact_info?.collected_count > 0 && (
- <span>⭐ {data.interact_info.collected_count}</span>
- )}
- {data.interact_info?.comment_count > 0 && (
- <span>💬 {data.interact_info.comment_count}</span>
- )}
- </div>
- </div>
- {/* 完整正文内容 */}
- {data.body_text && (
- <div style={{
- padding: '8px',
- background: 'white',
- borderRadius: '4px',
- marginBottom: '12px',
- fontSize: '11px',
- lineHeight: '1.5',
- border: '1px solid #fbbf24',
- whiteSpace: 'pre-wrap',
- wordBreak: 'break-word',
- }}>
- {data.body_text}
- </div>
- )}
- {/* AI分析 - 左右分栏 */}
- {data.extraction && data.extraction.images && (
- <div style={{
- display: 'flex',
- flexDirection: 'column',
- gap: '12px',
- }}>
- {data.extraction.images.map((img, idx) => (
- <div
- key={idx}
- style={{
- display: 'flex',
- flexDirection: 'row',
- gap: '16px',
- padding: '10px',
- background: 'white',
- borderRadius: '4px',
- border: '1px solid #d97706',
- alignItems: 'flex-start',
- }}
- >
- {/* 左侧:OCR提取文字 */}
- <div style={{
- flex: '1', // 1/3宽度
- minWidth: '0',
- }}>
- <div style={{
- fontSize: '11px',
- fontWeight: 'bold',
- color: '#92400e',
- marginBottom: '6px',
- }}>
- 📝 图片 {idx + 1}/{data.extraction.images.length}
- </div>
- {img.extract_text && (
- <div style={{
- fontSize: '11px',
- color: '#1f2937',
- lineHeight: '1.6',
- padding: '8px',
- background: '#fef9e7',
- borderRadius: '3px',
- borderLeft: '3px solid #f39c12',
- wordBreak: 'break-word',
- }}>
- <div style={{
- fontSize: '10px',
- fontWeight: 'bold',
- color: '#d97706',
- marginBottom: '4px',
- }}>
- 【提取文字】
- </div>
- {img.extract_text}
- </div>
- )}
- </div>
- {/* 右侧:缩略图 + 描述 */}
- <div style={{
- flex: '2', // 2/3宽度
- display: 'flex',
- flexDirection: 'column',
- gap: '8px',
- minWidth: '200px',
- }}>
- {/* 缩略图 */}
- {data.image_list && data.image_list[idx] && (
- <img
- src={(data.image_list[idx].image_url || data.image_list[idx])}
- alt={'图片' + (idx + 1)}
- style={{
- width: '100%',
- height: 'auto',
- maxHeight: '180px',
- objectFit: 'contain',
- borderRadius: '4px',
- border: '1px solid #d97706',
- cursor: 'pointer',
- }}
- onError={(e) => {
- e.target.style.display = 'none';
- }}
- />
- )}
- {/* 描述文字(完整展示) */}
- {img.description && (
- <div
- style={{
- fontSize: '10px',
- color: '#78350f',
- lineHeight: '1.5',
- wordBreak: 'break-word',
- padding: '8px',
- background: '#fef9e7',
- borderRadius: '3px',
- border: '1px solid #f39c12',
- }}
- >
- <div style={{
- fontSize: '9px',
- fontWeight: 'bold',
- color: '#d97706',
- marginBottom: '4px',
- }}>
- 【图片描述】
- </div>
- {img.description}
- </div>
- )}
- </div>
- </div>
- ))}
- </div>
- )}
- {/* 查看原帖链接 */}
- {data.note_url && (
- <div style={{ marginTop: '8px', fontSize: '10px' }}>
- <a
- href={data.note_url}
- target="_blank"
- rel="noopener noreferrer"
- style={{ color: '#92400e', textDecoration: 'underline' }}
- >
- 🔗 查看原帖
- </a>
- </div>
- )}
- <Handle
- type="source"
- position={Position.Right}
- style={{ background: '#fbbf24', width: 8, height: 8 }}
- />
- </div>
- );
- }
- const nodeTypes = {
- query: QueryNode,
- note: NoteNode,
- post: NoteNode, // 帖子节点使用 NoteNode 组件渲染
- analysis: AnalysisNode,
- };
- // 根据 score 获取颜色
- function getScoreColor(score) {
- if (score >= 0.7) return '#10b981'; // 绿色 - 高分
- if (score >= 0.4) return '#f59e0b'; // 橙色 - 中分
- return '#ef4444'; // 红色 - 低分
- }
- // 截断文本,保留头尾,中间显示省略号
- function truncateMiddle(text, maxLength = 20) {
- if (!text || text.length <= maxLength) return text;
- const headLength = Math.ceil(maxLength * 0.4);
- const tailLength = Math.floor(maxLength * 0.4);
- const head = text.substring(0, headLength);
- const tail = text.substring(text.length - tailLength);
- return \`\${head}...\${tail}\`;
- }
- // 根据策略获取颜色
- // 智能提取主要策略的辅助函数
- function getPrimaryStrategy(nodeData) {
- // 优先级1: 使用 primaryStrategy 字段
- if (nodeData.primaryStrategy) {
- return nodeData.primaryStrategy;
- }
- // 优先级2: 从 occurrences 数组中获取最新的策略
- if (nodeData.occurrences && Array.isArray(nodeData.occurrences) && nodeData.occurrences.length > 0) {
- const latestOccurrence = nodeData.occurrences[nodeData.occurrences.length - 1];
- if (latestOccurrence && latestOccurrence.strategy) {
- return latestOccurrence.strategy;
- }
- }
- // 优先级3: 拆分组合策略字符串,取第一个
- if (nodeData.strategy && typeof nodeData.strategy === 'string') {
- const strategies = nodeData.strategy.split(' + ');
- if (strategies.length > 0 && strategies[0]) {
- return strategies[0].trim();
- }
- }
- // 默认返回原始strategy或未知
- return nodeData.strategy || '未知';
- }
- function getStrategyColor(strategy) {
- const strategyColors = {
- '初始分词': '#10b981',
- '调用sug': '#06b6d4',
- '同义改写': '#f59e0b',
- '加词': '#3b82f6',
- '抽象改写': '#8b5cf6',
- '基于部分匹配改进': '#ec4899',
- '结果分支-抽象改写': '#a855f7',
- '结果分支-同义改写': '#fb923c',
- // v6.1.2.8 新增策略
- '原始问题': '#6b21a8',
- '来自分词': '#10b981',
- '加词生成': '#ef4444',
- '建议词': '#06b6d4',
- '执行搜索': '#8b5cf6',
- // 添加简化版本的策略映射
- '分词': '#10b981',
- '推荐词': '#06b6d4',
- };
- return strategyColors[strategy] || '#9ca3af';
- }
- // 树节点组件
- function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, onSelect }) {
- const hasChildren = children && children.length > 0;
- const score = node.data.score ? parseFloat(node.data.score) : 0;
- const strategy = getPrimaryStrategy(node.data); // 使用智能提取函数
- const strategyColor = getStrategyColor(strategy);
- const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型
- const isDomainCombination = nodeActualType === 'domain_combination';
- let sourceSummary = '';
- if (isDomainCombination && Array.isArray(node.data.source_word_details) && node.data.source_word_details.length > 0) {
- const summaryParts = [];
- node.data.source_word_details.forEach((detail) => {
- const words = Array.isArray(detail.words) ? detail.words : [];
- const wordTexts = [];
- words.forEach((w) => {
- const numericScore = typeof w.score === 'number' ? w.score : parseFloat(w.score || '0');
- const formattedScore = Number.isFinite(numericScore) ? numericScore.toFixed(2) : '0.00';
- wordTexts.push(w.text + ' (' + formattedScore + ')');
- });
- if (wordTexts.length > 0) {
- const segmentLabel = detail.segment_type ? '[' + detail.segment_type + '] ' : '';
- summaryParts.push(segmentLabel + wordTexts.join(' + '));
- }
- });
- sourceSummary = summaryParts.join(' | ');
- }
- // 计算字体颜色:根据分数提升幅度判断
- let fontColor = '#374151'; // 默认颜色
- if (node.type === 'note') {
- const evaluatorVersion = node.data.evaluator_version || '';
- if (evaluatorVersion === 'v3.0') {
- // V3评估:基于is_knowledge, is_content_knowledge和final_score判断颜色
- const isKnowledge = node.data.is_knowledge;
- const isContentKnowledge = node.data.is_content_knowledge;
- const finalScore = node.data.final_score;
- if (!isKnowledge || !isContentKnowledge) {
- fontColor = '#ef4444'; // 红色 - 非知识或非内容知识
- } else if (finalScore !== null && finalScore !== undefined) {
- if (finalScore >= 60) {
- fontColor = '#22c55e'; // 绿色 - 内容知识且高分
- } else {
- fontColor = '#eab308'; // 黄色 - 内容知识但分数偏低
- }
- }
- } else {
- // V2评估:基于知识得分和相关性得分判断颜色
- const knowledgeScore = node.data.knowledge_score;
- const relevanceScore = node.data.post_relevance_score;
- if (knowledgeScore != null && relevanceScore != null) {
- if (knowledgeScore <= 40) {
- fontColor = '#ef4444'; // 红色 - 知识得分低
- } else if (knowledgeScore > 40 && relevanceScore > 40) {
- fontColor = '#22c55e'; // 绿色 - 知识和相关性都高
- } else {
- fontColor = '#eab308'; // 黄色 - 知识得分高但相关性低
- }
- } else {
- // V1兼容:如果没有V2评估数据,使用matchLevel判断
- fontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
- }
- }
- } else if (node.data.seed_score !== undefined) {
- const parentScore = parseFloat(node.data.seed_score);
- const gain = score - parentScore;
- fontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
- } else if (node.data.isSelected === false) {
- fontColor = '#ef4444';
- }
- return (
- <div style={{ marginLeft: level * 12 + 'px', marginBottom: '8px' }}>
- <div
- style={{
- padding: '6px 8px',
- borderRadius: '4px',
- cursor: 'pointer',
- background: 'transparent',
- border: isSelected ? '1px solid #3b82f6' : '1px solid transparent',
- display: 'flex',
- alignItems: 'center',
- gap: '6px',
- transition: 'all 0.2s ease',
- position: 'relative',
- overflow: 'visible',
- }}
- onMouseEnter={(e) => {
- if (!isSelected) e.currentTarget.style.background = '#f9fafb';
- }}
- onMouseLeave={(e) => {
- if (!isSelected) e.currentTarget.style.background = 'transparent';
- }}
- >
- {/* 策略类型竖线 */}
- <div style={{
- width: '3px',
- height: '20px',
- background: strategyColor,
- borderRadius: '2px',
- flexShrink: 0,
- position: 'relative',
- zIndex: 1,
- }} />
- {hasChildren && (
- <span
- style={{
- fontSize: '10px',
- color: '#6b7280',
- cursor: 'pointer',
- width: '16px',
- textAlign: 'center',
- position: 'relative',
- zIndex: 1,
- }}
- onClick={(e) => {
- e.stopPropagation();
- onToggle();
- }}
- >
- {isCollapsed ? '▶' : '▼'}
- </span>
- )}
- {!hasChildren && <span style={{ width: '16px', position: 'relative', zIndex: 1 }}></span>}
- <div
- style={{
- flex: 1,
- fontSize: '12px',
- color: '#374151',
- position: 'relative',
- zIndex: 1,
- minWidth: 0,
- display: 'flex',
- flexDirection: 'column',
- gap: '4px',
- }}
- onClick={onSelect}
- >
- <div style={{
- display: 'flex',
- alignItems: 'center',
- gap: '8px',
- }}>
- {/* 文本标题 - 左侧 */}
- <div style={{
- fontWeight: level === 0 ? '600' : '400',
- flex: 1,
- minWidth: 0,
- color: node.data.scoreColor || fontColor,
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- }}
- title={node.data.title || node.id}
- >
- {node.data.title || node.id}
- </div>
- {/* 域标识 - 右侧,挨着分数,优先显示域类型,否则显示域索引或域字符串,但domain_combination节点不显示 */}
- {(node.data.domain_type || node.data.domains_str || (node.data.domain_index !== null && node.data.domain_index !== undefined)) && nodeActualType !== 'domain_combination' && (
- <span style={{
- fontSize: '12px',
- color: '#fff',
- background: '#6366f1',
- padding: '2px 5px',
- borderRadius: '3px',
- flexShrink: 0,
- fontWeight: '600',
- marginLeft: '4px',
- }}
- title={
- node.data.domain_type ? '域: ' + node.data.domain_type + ' (D' + node.data.domain_index + ')' :
- node.data.domains_str ? '域: ' + node.data.domains_str :
- '域 D' + node.data.domain_index
- }
- >
- {node.data.domain_type || node.data.domains_str || ('D' + node.data.domain_index)}
- </span>
- )}
- {node.data.is_suggestion && node.data.suggestion_label && (
- <span style={{
- fontSize: '12px',
- color: '#fff',
- background: '#8b5cf6',
- padding: '2px 5px',
- borderRadius: '3px',
- flexShrink: 0,
- fontWeight: '600',
- }}
- >
- {node.data.suggestion_label}
- </span>
- )}
- {/* 类型标签 - 显示在右侧靠近分数,蓝色背景 */}
- {node.data.type_label && (
- <span style={{
- fontSize: '12px',
- color: '#fff',
- background: '#6366f1',
- padding: '2px 5px',
- borderRadius: '3px',
- flexShrink: 0,
- fontWeight: '600',
- }}
- title={'类型: ' + node.data.type_label}
- >
- {node.data.type_label}
- </span>
- )}
- {/* 分数显示 - 步骤和轮次节点不显示分数 */}
- {nodeActualType !== 'step' && nodeActualType !== 'round' && (
- <span style={{
- fontSize: '11px',
- color: '#6b7280',
- fontWeight: '500',
- flexShrink: 0,
- minWidth: '35px',
- textAlign: 'right',
- }}>
- {node.type === 'note' && node.data.evaluator_version === 'v3.0' && node.data.final_score !== null && node.data.final_score !== undefined
- ? node.data.final_score.toFixed(1)
- : score.toFixed(2)}
- </span>
- )}
- </div>
- {/* V3评估信息行 - 仅对note类型且有V3评估数据显示 */}
- {node.type === 'note' && node.data.evaluator_version === 'v3.0' && (
- <div style={{ fontSize: '10px', color: '#2563eb', marginTop: '2px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
- <span style={{ fontWeight: '600', color: '#2563eb' }}>评估结论:</span>
- <span style={{ color: node.data.is_knowledge ? '#16a34a' : '#dc2626', fontWeight: '500' }}>
- {node.data.is_knowledge ? '✓ 是知识' : '✗ 非知识'}
- </span>
- {node.data.is_content_knowledge !== null && node.data.is_content_knowledge !== undefined && (
- <span style={{ color: node.data.is_content_knowledge ? '#16a34a' : '#dc2626', fontWeight: '500' }}>
- | {node.data.is_content_knowledge ? '✓ 是内容知识' : '✗ 非内容知识'}
- </span>
- )}
- {node.data.is_content_knowledge && node.data.final_score !== null && node.data.final_score !== undefined && (
- <>
- <span style={{ fontWeight: '500', color: '#2563eb' }}>| {node.data.match_level}</span>
- <span style={{ fontWeight: '600', color: node.data.final_score >= 60 ? '#16a34a' : '#ea580c' }}>
- | {node.data.final_score.toFixed(1)}分
- </span>
- </>
- )}
- </div>
- )}
- {/* 域组合的来源词得分(树状视图,右对齐) */}
- {isDomainCombination && sourceSummary && (
- <div style={{
- fontSize: '10px',
- color: '#2563eb',
- lineHeight: '1.4',
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'flex-end',
- gap: '2px',
- textAlign: 'right',
- }}>
- {node.data.source_word_details.map((detail, idx) => {
- const words = Array.isArray(detail.words) ? detail.words : [];
- const summary = words.map((w) => {
- const numericScore = typeof w.score === 'number' ? w.score : parseFloat(w.score || '0');
- const formattedScore = Number.isFinite(numericScore) ? numericScore.toFixed(2) : '0.00';
- return w.text + ' (' + formattedScore + ')';
- }).join(' + ');
- return (
- <span key={idx} title={summary}>
- {summary}
- </span>
- );
- })}
- </div>
- )}
- {/* 分数下划线 - 步骤和轮次节点不显示 */}
- {nodeActualType !== 'step' && nodeActualType !== 'round' && (
- <div style={{
- width: (score * 100) + '%',
- height: '2px',
- background: getScoreColor(score),
- borderRadius: '1px',
- }} />
- )}
- </div>
- </div>
- {hasChildren && !isCollapsed && (
- <div>
- {children}
- </div>
- )}
- </div>
- );
- }
- // 使用 dagre 自动布局
- function getLayoutedElements(nodes, edges, direction = 'LR') {
- console.log('🎯 Starting layout with dagre...');
- console.log('Input:', nodes.length, 'nodes,', edges.length, 'edges');
- // 检查 dagre 是否加载
- if (typeof window === 'undefined' || typeof window.dagre === 'undefined') {
- console.warn('⚠️ Dagre not loaded, using fallback layout');
- // 降级到简单布局
- const levelGroups = {};
- nodes.forEach(node => {
- const level = node.data.level || 0;
- if (!levelGroups[level]) levelGroups[level] = [];
- levelGroups[level].push(node);
- });
- Object.entries(levelGroups).forEach(([level, nodeList]) => {
- const x = parseInt(level) * 480;
- nodeList.forEach((node, index) => {
- node.position = { x, y: index * 260 };
- node.targetPosition = 'left';
- node.sourcePosition = 'right';
- });
- });
- return { nodes, edges };
- }
- try {
- const dagreGraph = new window.dagre.graphlib.Graph();
- dagreGraph.setDefaultEdgeLabel(() => ({}));
- const isHorizontal = direction === 'LR';
- dagreGraph.setGraph({
- rankdir: direction,
- nodesep: 800, // 垂直间距 - 增加以适应更高的note节点(卡片高度2600px + 800px间距)
- ranksep: 400, // 水平间距 - 增加以容纳更宽的节点
- });
- // 添加节点 - 根据节点类型设置不同的尺寸
- nodes.forEach((node) => {
- let nodeWidth = 320;
- let nodeHeight = 220;
- // note 节点有轮播图,需要更大的空间
- if (node.type === 'note') {
- nodeWidth = 360;
- nodeHeight = 2600; // 更新以适应完整内容:1:1图片(880px) + 标题/原始问题/评估(500px) + 正文/AI提取(最多1200px)
- }
- // analysis 节点内容很多,需要更大的空间
- else if (node.type === 'analysis') {
- nodeWidth = 900; // 宽度足够容纳左右分栏
- nodeHeight = 600; // 高度足够容纳多张图片
- }
- dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
- });
- // 添加边
- edges.forEach((edge) => {
- dagreGraph.setEdge(edge.source, edge.target);
- });
- // 计算布局
- window.dagre.layout(dagreGraph);
- console.log('✅ Dagre layout completed');
- // 更新节点位置和 handle 位置
- nodes.forEach((node) => {
- const nodeWithPosition = dagreGraph.node(node.id);
- if (!nodeWithPosition) {
- console.warn('Node position not found for:', node.id);
- return;
- }
- node.targetPosition = isHorizontal ? 'left' : 'top';
- node.sourcePosition = isHorizontal ? 'right' : 'bottom';
- // 根据节点类型获取尺寸
- let nodeWidth = 320;
- let nodeHeight = 220;
- if (node.type === 'note') {
- nodeWidth = 360;
- nodeHeight = 2600; // 与dagre布局参数保持一致
- }
- // 将 dagre 的中心点位置转换为 React Flow 的左上角位置
- node.position = {
- x: nodeWithPosition.x - nodeWidth / 2,
- y: nodeWithPosition.y - nodeHeight / 2,
- };
- });
- // 为同层级的 note 节点添加交错偏移,避免视觉重叠
- console.log('=== 开始交错偏移逻辑 ===');
- console.log('总节点数:', nodes.length);
- const noteNodes = nodes.filter(n => n.type === 'note');
- console.log('过滤后的 note 节点数:', noteNodes.length);
- if (noteNodes.length > 1) {
- // 输出排序前的位置
- console.log('排序前的 note 节点位置:');
- noteNodes.forEach((n, i) => {
- console.log(' [' + i + '] ' + n.id.substring(0, 40) + '... | type=' + n.type + ' | pos=(' + n.position.x.toFixed(0) + ', ' + n.position.y.toFixed(0) + ')');
- });
- // 按 Y 坐标排序
- noteNodes.sort((a, b) => a.position.y - b.position.y);
- console.log('排序后的 note 节点位置:');
- noteNodes.forEach((n, i) => {
- console.log(' [' + i + '] ' + n.id.substring(0, 40) + '... | pos=(' + n.position.x.toFixed(0) + ', ' + n.position.y.toFixed(0) + ')');
- });
- // 为相邻的 note 节点添加 X 方向的交错(3个位置:左、中、右)
- const baseX = noteNodes.length > 0 ? noteNodes[0].position.x : 0;
- const leftX = baseX - 1500;
- const centerX = baseX;
- const rightX = baseX + 1500;
- let appliedCount = 0;
- noteNodes.forEach((node, index) => {
- const oldX = node.position.x;
- const position = index % 3;
- if (position === 0) {
- node.position.x = leftX;
- console.log(' [' + index + '] 设置为左侧: X ' + oldX.toFixed(0) + ' → ' + node.position.x.toFixed(0));
- } else if (position === 1) {
- node.position.x = centerX;
- console.log(' [' + index + '] 设置为中间: X ' + oldX.toFixed(0) + ' → ' + node.position.x.toFixed(0));
- } else {
- node.position.x = rightX;
- console.log(' [' + index + '] 设置为右侧: X ' + oldX.toFixed(0) + ' → ' + node.position.x.toFixed(0));
- }
- appliedCount++;
- });
- console.log('总共应用了 ' + appliedCount + ' 次偏移');
- } else {
- console.log('note 节点数量 <= 1,不需要交错');
- }
- console.log('=== 交错偏移逻辑结束 ===');
- console.log('✅ Layout completed, sample node:', nodes[0]);
- return { nodes, edges };
- } catch (error) {
- console.error('❌ Error in dagre layout:', error);
- console.error('Error details:', error.message, error.stack);
- // 降级处理
- console.log('Using fallback layout...');
- const levelGroups = {};
- nodes.forEach(node => {
- const level = node.data.level || 0;
- if (!levelGroups[level]) levelGroups[level] = [];
- levelGroups[level].push(node);
- });
- Object.entries(levelGroups).forEach(([level, nodeList]) => {
- const x = parseInt(level) * 480;
- nodeList.forEach((node, index) => {
- node.position = { x, y: index * 260 };
- node.targetPosition = 'left';
- node.sourcePosition = 'right';
- });
- });
- return { nodes, edges };
- }
- }
- function transformData(data) {
- const nodes = [];
- const edges = [];
- const originalIdToCanvasId = {}; // 原始ID -> 画布ID的映射
- const canvasIdToNodeData = {}; // 避免重复创建相同的节点
- let analysisNodeCount = 0; // 用于给analysis节点添加X偏移
- // 🆕 获取原始问题(从root节点)
- const originalQuestion = data.nodes['root_o']?.query || '';
- // 创建节点
- Object.entries(data.nodes).forEach(([originalId, node]) => {
- // 统一处理所有类型的节点
- const nodeType = node.type || 'query';
- // 直接使用originalId作为canvasId,避免冲突
- const canvasId = originalId;
- originalIdToCanvasId[originalId] = canvasId;
- // 如果这个 canvasId 还没有创建过节点,则创建
- if (!canvasIdToNodeData[canvasId]) {
- canvasIdToNodeData[canvasId] = true;
- // 根据节点类型创建不同的数据结构
- if (nodeType === 'note' || nodeType === 'post') {
- nodes.push({
- id: canvasId,
- originalId: originalId,
- type: 'note',
- data: {
- title: node.query || node.title || '帖子',
- matchLevel: node.match_level,
- score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00',
- description: node.body_text || node.desc || '',
- isSelected: node.is_selected !== undefined ? node.is_selected : true,
- imageList: node.image_list || [],
- note_url: node.note_url || '',
- evaluationReason: node.evaluationReason || node.evaluation_reason || '',
- interact_info: node.interact_info || {},
- nodeType: nodeType,
- // 🆕 评估字段 (V2)
- // 知识评估
- is_knowledge: node.is_knowledge !== undefined ? node.is_knowledge : null,
- knowledge_reason: node.knowledge_reason || '',
- knowledge_score: node.knowledge_score !== undefined ? node.knowledge_score : null,
- knowledge_level: node.knowledge_level !== undefined ? node.knowledge_level : null,
- knowledge_evaluation: node.knowledge_evaluation || null,
- // 相关性评估
- post_relevance_score: node.post_relevance_score !== undefined ? node.post_relevance_score : null,
- relevance_level: node.relevance_level || '',
- relevance_reason: node.relevance_reason || '',
- relevance_conclusion: node.relevance_conclusion || '',
- relevance_evaluation: node.relevance_evaluation || null,
- // 🆕 评估字段 (V3)
- is_content_knowledge: node.is_content_knowledge !== undefined ? node.is_content_knowledge : null,
- purpose_score: node.purpose_score !== undefined ? node.purpose_score : null,
- category_score: node.category_score !== undefined ? node.category_score : null,
- final_score: node.final_score !== undefined ? node.final_score : null,
- match_level: node.match_level || '',
- evaluator_version: node.evaluator_version || '',
- content_knowledge_evaluation: node.content_knowledge_evaluation || null,
- purpose_evaluation: node.purpose_evaluation || null,
- category_evaluation: node.category_evaluation || null,
- // 🆕 原始问题
- originalQuestion: originalQuestion
- },
- position: { x: 0, y: 0 },
- });
- } else if (nodeType === 'analysis') {
- // AI分析节点 - 添加X偏移避免叠加
- const xOffset = analysisNodeCount * 150; // 每个节点偏移150px
- analysisNodeCount++;
- nodes.push({
- id: canvasId,
- originalId: originalId,
- type: 'analysis',
- data: {
- query: node.query || '[AI分析]',
- note_id: node.note_id,
- note_url: node.note_url,
- title: node.title || '',
- body_text: node.body_text || '',
- interact_info: node.interact_info || {},
- extraction: node.extraction || null,
- image_list: node.image_list || [],
- },
- position: { x: xOffset, y: 0 },
- });
- } else {
- // query, seg, q, search, root 等节点
- let displayTitle = node.query || originalId;
- nodes.push({
- id: canvasId,
- originalId: originalId,
- type: 'query', // 使用 query 组件渲染所有非note节点
- data: {
- title: displayTitle,
- level: node.level || 0,
- score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00',
- strategy: node.strategy || '',
- parent: node.parent_query || '',
- isSelected: node.is_selected !== undefined ? node.is_selected : true,
- evaluationReason: node.evaluationReason || node.evaluation_reason || '',
- nodeType: nodeType, // 传递实际节点类型用于样式
- searchCount: node.search_count, // search 节点特有
- totalPosts: node.total_posts, // search 节点特有
- selectedWord: node.selected_word || '', // 加词节点特有 - 显示选择的词
- scoreColor: node.scoreColor || null, // SUG节点的颜色标识
- parentQScore: node.parentQScore || 0, // 父Q得分(用于调试)
- domain_index: node.domain_index !== undefined ? node.domain_index : null, // 域索引
- domain_type: node.domain_type || '', // 域类型(如"中心名词"、"核心动作"),只有Q节点有,segment节点不显示
- segment_type: node.segment_type || '', // segment类型(只有segment节点才有)
- type_label: node.type_label || '', // 类型标签
- domains: node.domains || [], // 域索引数组(domain_combination节点特有)
- domains_str: node.domains_str || '', // 域标识字符串(如"D0,D1")
- from_segments: node.from_segments || [], // 来源segments(domain_combination节点特有)
- source_word_details: node.source_word_details || [], // 组合来源词及其得分
- source_scores: node.source_scores || [], // 扁平来源得分
- is_above_sources: node.is_above_sources || false, // 组合是否高于来源得分
- max_source_score: node.max_source_score !== undefined ? node.max_source_score : null, // 来源最高分
- item_type: node.item_type || '', // 构建下一轮节点来源类型
- is_suggestion: node.is_suggestion || false,
- suggestion_label: node.suggestion_label || '',
- },
- position: { x: 0, y: 0 },
- });
- }
- }
- });
- // 创建边 - 使用虚线样式,映射到画布ID
- data.edges.forEach((edge, index) => {
- const edgeColors = {
- '初始分词': '#10b981',
- '调用sug': '#06b6d4',
- '同义改写': '#f59e0b',
- '加词': '#3b82f6',
- '抽象改写': '#8b5cf6',
- '基于部分匹配改进': '#ec4899',
- '结果分支-抽象改写': '#a855f7',
- '结果分支-同义改写': '#fb923c',
- 'query_to_note': '#ec4899',
- };
- const color = edgeColors[edge.strategy] || edgeColors[edge.edge_type] || '#d1d5db';
- const isNoteEdge = edge.edge_type === 'query_to_note';
- edges.push({
- id: \`edge-\${index}\`,
- source: originalIdToCanvasId[edge.from], // 使用画布ID
- target: originalIdToCanvasId[edge.to], // 使用画布ID
- type: 'simplebezier', // 使用简单贝塞尔曲线
- animated: isNoteEdge,
- style: {
- stroke: color,
- strokeWidth: isNoteEdge ? 2.5 : 2,
- strokeDasharray: isNoteEdge ? '5,5' : '8,4',
- },
- markerEnd: {
- type: 'arrowclosed',
- color: color,
- width: 20,
- height: 20,
- },
- });
- });
- // 使用 dagre 自动计算布局 - 从左到右
- return getLayoutedElements(nodes, edges, 'LR');
- }
- function FlowContent() {
- // 画布使用简化数据
- const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
- console.log('🔍 Transforming data for canvas...');
- const result = transformData(data);
- console.log('✅ Canvas data:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
- return result;
- }, []);
- // 目录使用完整数据(如果存在)
- const { nodes: fullNodes, edges: fullEdges } = useMemo(() => {
- if (data.fullData) {
- console.log('🔍 Transforming full data for tree directory...');
- const result = transformData(data.fullData);
- console.log('✅ Directory data:', result.nodes.length, 'nodes,', result.edges.length, 'edges');
- return result;
- }
- // 如果没有 fullData,使用简化数据
- return { nodes: initialNodes, edges: initialEdges };
- }, [initialNodes, initialEdges]);
- // 初始化:找出所有有子节点的节点,默认折叠(画布节点)
- const initialCollapsedNodes = useMemo(() => {
- const nodesWithChildren = new Set();
- initialEdges.forEach(edge => {
- nodesWithChildren.add(edge.source);
- });
- // 排除根节点(level 0),让根节点默认展开
- const rootNode = initialNodes.find(n => n.data.level === 0);
- if (rootNode) {
- nodesWithChildren.delete(rootNode.id);
- }
- return nodesWithChildren;
- }, [initialNodes, initialEdges]);
- // 树节点的折叠状态需要在树构建后初始化
- const [collapsedNodes, setCollapsedNodes] = useState(() => initialCollapsedNodes);
- const [collapsedTreeNodes, setCollapsedTreeNodes] = useState(new Set());
- const [selectedNodeId, setSelectedNodeId] = useState(null);
- const [hiddenNodes, setHiddenNodes] = useState(new Set()); // 用户手动隐藏的节点
- const [focusMode, setFocusMode] = useState(false); // 全局聚焦模式,默认关闭
- const [focusedNodeId, setFocusedNodeId] = useState(null); // 单独聚焦的节点ID
- const [sidebarWidth, setSidebarWidth] = useState(400); // 左侧目录宽度
- const [isResizing, setIsResizing] = useState(false); // 是否正在拖拽调整宽度
- // 拖拽调整侧边栏宽度的处理逻辑
- const handleMouseDown = useCallback(() => {
- setIsResizing(true);
- }, []);
- useEffect(() => {
- if (!isResizing) return;
- const handleMouseMove = (e) => {
- const newWidth = e.clientX;
- // 限制宽度范围:300px - 700px
- if (newWidth >= 300 && newWidth <= 700) {
- setSidebarWidth(newWidth);
- }
- };
- const handleMouseUp = () => {
- setIsResizing(false);
- };
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- };
- }, [isResizing]);
- // 获取 React Flow 实例以控制画布
- const { setCenter, fitView } = useReactFlow();
- // 获取某个节点的所有后代节点ID
- const getDescendants = useCallback((nodeId) => {
- const descendants = new Set();
- const queue = [nodeId];
- while (queue.length > 0) {
- const current = queue.shift();
- initialEdges.forEach(edge => {
- if (edge.source === current && !descendants.has(edge.target)) {
- descendants.add(edge.target);
- queue.push(edge.target);
- }
- });
- }
- return descendants;
- }, [initialEdges]);
- // 获取直接父节点
- const getDirectParents = useCallback((nodeId) => {
- const parents = [];
- initialEdges.forEach(edge => {
- if (edge.target === nodeId) {
- parents.push(edge.source);
- }
- });
- return parents;
- }, [initialEdges]);
- // 获取直接子节点
- const getDirectChildren = useCallback((nodeId) => {
- const children = [];
- initialEdges.forEach(edge => {
- if (edge.source === nodeId) {
- children.push(edge.target);
- }
- });
- return children;
- }, [initialEdges]);
- // 切换节点折叠状态
- const toggleNodeCollapse = useCallback((nodeId) => {
- setCollapsedNodes(prev => {
- const newSet = new Set(prev);
- const descendants = getDescendants(nodeId);
- if (newSet.has(nodeId)) {
- // 展开:移除此节点,但保持其他折叠的节点
- newSet.delete(nodeId);
- } else {
- // 折叠:添加此节点
- newSet.add(nodeId);
- }
- return newSet;
- });
- }, [getDescendants]);
- // 过滤可见的节点和边,并重新计算布局
- const { nodes, edges } = useMemo(() => {
- const nodesToHide = new Set();
- // 判断使用哪个节点ID进行聚焦:优先使用单独聚焦的节点,否则使用全局聚焦模式的选中节点
- const effectiveFocusNodeId = focusedNodeId || (focusMode ? selectedNodeId : null);
- // 聚焦模式:只显示聚焦节点、其父节点和直接子节点
- if (effectiveFocusNodeId) {
- const visibleInFocus = new Set([effectiveFocusNodeId]);
- // 添加所有父节点
- initialEdges.forEach(edge => {
- if (edge.target === effectiveFocusNodeId) {
- visibleInFocus.add(edge.source);
- }
- });
- // 添加所有直接子节点
- initialEdges.forEach(edge => {
- if (edge.source === effectiveFocusNodeId) {
- visibleInFocus.add(edge.target);
- }
- });
- // 隐藏不在聚焦范围内的节点
- initialNodes.forEach(node => {
- if (!visibleInFocus.has(node.id)) {
- nodesToHide.add(node.id);
- }
- });
- } else {
- // 非聚焦模式:使用原有的折叠逻辑
- // 收集所有被折叠节点的后代
- collapsedNodes.forEach(collapsedId => {
- const descendants = getDescendants(collapsedId);
- descendants.forEach(id => nodesToHide.add(id));
- });
- }
- // 添加用户手动隐藏的节点
- hiddenNodes.forEach(id => nodesToHide.add(id));
- const visibleNodes = initialNodes
- .filter(node => !nodesToHide.has(node.id))
- .map(node => ({
- ...node,
- data: {
- ...node.data,
- isCollapsed: collapsedNodes.has(node.id),
- hasChildren: initialEdges.some(e => e.source === node.id),
- onToggleCollapse: () => toggleNodeCollapse(node.id),
- onHideSelf: () => {
- setHiddenNodes(prev => {
- const newSet = new Set(prev);
- newSet.add(node.id);
- return newSet;
- });
- },
- onFocus: () => {
- // 切换聚焦状态
- if (focusedNodeId === node.id) {
- setFocusedNodeId(null); // 如果已经聚焦,则取消聚焦
- } else {
- // 先取消之前的聚焦,然后聚焦到当前节点
- setFocusedNodeId(node.id);
- // 延迟聚焦视图到该节点
- setTimeout(() => {
- fitView({
- nodes: [{ id: node.id }],
- duration: 800,
- padding: 0.3,
- });
- }, 100);
- }
- },
- isFocused: focusedNodeId === node.id,
- isHighlighted: selectedNodeId === node.id,
- }
- }));
- const visibleEdges = initialEdges.filter(
- edge => !nodesToHide.has(edge.source) && !nodesToHide.has(edge.target)
- );
- // 重新计算布局 - 只对可见节点
- if (typeof window !== 'undefined' && typeof window.dagre !== 'undefined') {
- try {
- const dagreGraph = new window.dagre.graphlib.Graph();
- dagreGraph.setDefaultEdgeLabel(() => ({}));
- dagreGraph.setGraph({
- rankdir: 'LR',
- nodesep: 800, // 与static layout保持一致,确保不重叠
- ranksep: 400, // 增加水平间距
- });
- visibleNodes.forEach((node) => {
- let nodeWidth = 320;
- let nodeHeight = 220;
- // note 节点有轮播图,需要更大的空间
- if (node.type === 'note') {
- nodeWidth = 360;
- nodeHeight = 2600; // 与static layout保持一致
- }
- dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
- });
- visibleEdges.forEach((edge) => {
- dagreGraph.setEdge(edge.source, edge.target);
- });
- window.dagre.layout(dagreGraph);
- visibleNodes.forEach((node) => {
- const nodeWithPosition = dagreGraph.node(node.id);
- if (nodeWithPosition) {
- // 根据节点类型获取对应的尺寸
- let nodeWidth = 320;
- let nodeHeight = 220;
- if (node.type === 'note') {
- nodeWidth = 360;
- nodeHeight = 2600; // 与static layout保持一致
- }
- node.position = {
- x: nodeWithPosition.x - nodeWidth / 2,
- y: nodeWithPosition.y - nodeHeight / 2,
- };
- node.targetPosition = 'left';
- node.sourcePosition = 'right';
- }
- });
- // 为同层级的 note 节点添加交错偏移,避免视觉重叠
- console.log('[DYNAMIC LAYOUT] 开始应用交错偏移');
- const noteNodesToStagger = visibleNodes.filter(n => n.type === 'note');
- console.log('[DYNAMIC LAYOUT] note 节点数:', noteNodesToStagger.length);
- if (noteNodesToStagger.length > 1) {
- // 按 Y 坐标排序
- noteNodesToStagger.sort((a, b) => a.position.y - b.position.y);
- console.log('[DYNAMIC LAYOUT] 排序后准备应用偏移:');
- noteNodesToStagger.forEach((n, i) => {
- console.log(' [' + i + '] ' + n.id.substring(0, 40) + '... | pos=(' + n.position.x.toFixed(0) + ', ' + n.position.y.toFixed(0) + ')');
- });
- // 为相邻的 note 节点添加 X 方向的交错(3个位置:左、中、右)
- const baseX = noteNodesToStagger.length > 0 ? noteNodesToStagger[0].position.x : 0;
- const leftX = baseX - 1500;
- const centerX = baseX;
- const rightX = baseX + 1500;
- let appliedCount = 0;
- noteNodesToStagger.forEach((node, index) => {
- const oldX = node.position.x;
- const position = index % 3;
- if (position === 0) {
- node.position.x = leftX;
- console.log('[DYNAMIC LAYOUT] [' + index + '] 设置为左侧: X ' + oldX.toFixed(0) + ' → ' + node.position.x.toFixed(0));
- } else if (position === 1) {
- node.position.x = centerX;
- console.log('[DYNAMIC LAYOUT] [' + index + '] 设置为中间: X ' + oldX.toFixed(0) + ' → ' + node.position.x.toFixed(0));
- } else {
- node.position.x = rightX;
- console.log('[DYNAMIC LAYOUT] [' + index + '] 设置为右侧: X ' + oldX.toFixed(0) + ' → ' + node.position.x.toFixed(0));
- }
- appliedCount++;
- });
- console.log('[DYNAMIC LAYOUT] 总共应用了 ' + appliedCount + ' 次偏移');
- }
- console.log('✅ Dynamic layout recalculated for', visibleNodes.length, 'visible nodes');
- } catch (error) {
- console.error('❌ Error in dynamic layout:', error);
- }
- }
- return { nodes: visibleNodes, edges: visibleEdges };
- }, [initialNodes, initialEdges, collapsedNodes, hiddenNodes, focusMode, focusedNodeId, getDescendants, toggleNodeCollapse, selectedNodeId]);
- // 构建树形结构 - 允许一个节点有多个父节点
- // 为目录构建树(使用完整数据)
- const buildTree = useCallback(() => {
- // 使用完整数据构建目录树
- const nodeMap = new Map();
- fullNodes.forEach(node => {
- nodeMap.set(node.id, node);
- });
- // 为每个节点创建树节点的副本(允许多次出现)
- const createTreeNode = (nodeId, pathKey) => {
- const node = nodeMap.get(nodeId);
- if (!node) return null;
- return {
- ...node,
- treeKey: pathKey, // 唯一的树路径key,用于React key
- children: []
- };
- };
- // 构建父子关系映射:记录每个节点的所有父节点,去重边
- const parentToChildren = new Map();
- const childToParents = new Map();
- fullEdges.forEach(edge => {
- // 记录父->子关系(去重:同一个父节点到同一个子节点只记录一次)
- if (!parentToChildren.has(edge.source)) {
- parentToChildren.set(edge.source, []);
- }
- const children = parentToChildren.get(edge.source);
- if (!children.includes(edge.target)) {
- children.push(edge.target);
- }
- // 记录子->父关系(用于判断是否有多个父节点,也去重)
- if (!childToParents.has(edge.target)) {
- childToParents.set(edge.target, []);
- }
- const parents = childToParents.get(edge.target);
- if (!parents.includes(edge.source)) {
- parents.push(edge.source);
- }
- });
- // 递归构建树
- const buildSubtree = (nodeId, pathKey, visitedInPath) => {
- // 避免循环引用:如果当前路径中已经访问过这个节点,跳过
- if (visitedInPath.has(nodeId)) {
- return null;
- }
- const treeNode = createTreeNode(nodeId, pathKey);
- if (!treeNode) return null;
- const newVisitedInPath = new Set(visitedInPath);
- newVisitedInPath.add(nodeId);
- const children = parentToChildren.get(nodeId) || [];
- treeNode.children = children
- .map((childId, index) => buildSubtree(childId, pathKey + '-' + childId + '-' + index, newVisitedInPath))
- .filter(child => child !== null);
- return treeNode;
- };
- // 找出所有根节点(没有入边的节点)
- const hasParent = new Set();
- fullEdges.forEach(edge => {
- hasParent.add(edge.target);
- });
- const roots = [];
- fullNodes.forEach((node, index) => {
- if (!hasParent.has(node.id)) {
- const treeNode = buildSubtree(node.id, 'root-' + node.id + '-' + index, new Set());
- if (treeNode) roots.push(treeNode);
- }
- });
- return roots;
- }, [fullNodes, fullEdges]);
- const treeRoots = useMemo(() => buildTree(), [buildTree]);
- // 生成树形文本结构(使用完整数据)
- const generateTreeText = useCallback(() => {
- const lines = [];
- // 递归生成树形文本
- const traverse = (nodes, prefix = '', isLast = true, depth = 0) => {
- nodes.forEach((node, index) => {
- const isLastNode = index === nodes.length - 1;
- const nodeData = fullNodes.find(n => n.id === node.id)?.data || {};
- const nodeType = nodeData.nodeType || node.data?.nodeType || 'unknown';
- const title = nodeData.title || node.data?.title || node.id;
- // 优先从node.data获取score,然后从nodeData获取
- let score = null;
- if (node.data?.score !== undefined && node.data?.score !== null) {
- score = node.data.score;
- } else if (node.data?.relevance_score !== undefined && node.data?.relevance_score !== null) {
- score = node.data.relevance_score;
- } else if (nodeData.score !== undefined && nodeData.score !== null) {
- score = nodeData.score;
- } else if (nodeData.relevance_score !== undefined && nodeData.relevance_score !== null) {
- score = nodeData.relevance_score;
- }
- const strategy = nodeData.strategy || node.data?.strategy || '';
- // 构建当前行 - score可能是数字或字符串,step/round节点不显示分数
- const connector = isLastNode ? '└─' : '├─';
- let scoreText = '';
- if (nodeType !== 'step' && nodeType !== 'round' && score !== null && score !== undefined) {
- // score可能已经是字符串格式(如 "0.05"),也可能是数字
- const scoreStr = typeof score === 'number' ? score.toFixed(2) : score;
- scoreText = \` (分数: \${scoreStr})\`;
- }
- const strategyText = strategy ? \` [\${strategy}]\` : '';
- lines.push(\`\${prefix}\${connector} \${title}\${scoreText}\${strategyText}\`);
- // 递归处理子节点
- if (node.children && node.children.length > 0) {
- const childPrefix = prefix + (isLastNode ? ' ' : '│ ');
- traverse(node.children, childPrefix, isLastNode, depth + 1);
- }
- });
- };
- // 添加标题
- const rootNode = fullNodes.find(n => n.data?.level === 0);
- if (rootNode) {
- lines.push(\`📊 查询扩展树形结构\`);
- lines.push(\`原始问题: \${rootNode.data.title || rootNode.data.query}\`);
- lines.push('');
- }
- traverse(treeRoots);
- return lines.join('\\n');
- }, [treeRoots, fullNodes]);
- // 复制树形结构到剪贴板
- const copyTreeToClipboard = useCallback(async () => {
- try {
- const treeText = generateTreeText();
- await navigator.clipboard.writeText(treeText);
- alert('✅ 树形结构已复制到剪贴板!');
- } catch (err) {
- console.error('复制失败:', err);
- alert('❌ 复制失败,请手动复制');
- }
- }, [generateTreeText]);
- // 初始化树节点折叠状态
- useEffect(() => {
- const getAllTreeKeys = (nodes) => {
- const keys = new Set();
- const traverse = (node) => {
- if (node.children && node.children.length > 0) {
- // 排除根节点
- if (node.data.level !== 0) {
- keys.add(node.treeKey);
- }
- node.children.forEach(traverse);
- }
- };
- nodes.forEach(traverse);
- return keys;
- };
- setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
- }, [treeRoots]);
- // 映射完整节点ID到画布简化节点ID
- const mapTreeNodeToCanvasNode = useCallback((treeNodeId) => {
- // 如果是简化模式,需要映射
- if (data.fullData) {
- // 从完整数据中找到节点
- const fullNode = fullNodes.find(n => n.id === treeNodeId);
- if (!fullNode) return treeNodeId;
- // 根据节点类型和文本找到画布上的简化节点
- const nodeText = fullNode.data.title || fullNode.data.query;
- const nodeType = fullNode.data.nodeType || fullNode.type;
- // Query类节点:找 query_xxx
- if (['q', 'seg', 'sug', 'add_word', 'query'].includes(nodeType)) {
- const canvasNode = initialNodes.find(n =>
- (n.data.title === nodeText || n.data.query === nodeText) &&
- ['query'].includes(n.type)
- );
- return canvasNode ? canvasNode.id : treeNodeId;
- }
- // Post节点:按note_id查找
- if (nodeType === 'post' || nodeType === 'note') {
- const noteId = fullNode.data.note_id;
- if (noteId) {
- const canvasNode = initialNodes.find(n => n.data.note_id === noteId);
- return canvasNode ? canvasNode.id : treeNodeId;
- }
- }
- // 其他节点类型(Round/Step等):直接返回
- return treeNodeId;
- }
- // 非简化模式,直接返回
- return treeNodeId;
- }, [data.fullData, fullNodes, initialNodes]);
- const renderTree = useCallback((treeNodes, level = 0) => {
- return treeNodes.map(node => {
- // 使用 treeKey 来区分树中的不同实例
- const isCollapsed = collapsedTreeNodes.has(node.treeKey);
- const isSelected = selectedNodeId === node.id;
- return (
- <TreeNode
- key={node.treeKey}
- node={node}
- level={level}
- isCollapsed={isCollapsed}
- isSelected={isSelected}
- onToggle={() => {
- setCollapsedTreeNodes(prev => {
- const newSet = new Set(prev);
- if (newSet.has(node.treeKey)) {
- newSet.delete(node.treeKey);
- } else {
- newSet.add(node.treeKey);
- }
- return newSet;
- });
- }}
- onSelect={() => {
- // 将目录节点ID映射到画布节点ID
- const treeNodeId = node.id;
- const canvasNodeId = mapTreeNodeToCanvasNode(treeNodeId);
- // 检查画布上是否存在这个节点
- const canvasNodeExists = initialNodes.some(n => n.id === canvasNodeId);
- if (!canvasNodeExists) {
- console.warn(\`节点 \${canvasNodeId} 在画布上不存在(可能被简化了)\`);
- return;
- }
- const nodeId = canvasNodeId;
- // 展开所有祖先节点
- const ancestorIds = [nodeId];
- const findAncestors = (id) => {
- initialEdges.forEach(edge => {
- if (edge.target === id && !ancestorIds.includes(edge.source)) {
- ancestorIds.push(edge.source);
- findAncestors(edge.source);
- }
- });
- };
- findAncestors(nodeId);
- // 如果节点或其祖先被隐藏,先恢复它们
- setHiddenNodes(prev => {
- const newSet = new Set(prev);
- ancestorIds.forEach(id => newSet.delete(id));
- return newSet;
- });
- setSelectedNodeId(nodeId);
- // 获取选中节点的直接子节点
- const childrenIds = [];
- initialEdges.forEach(edge => {
- if (edge.source === nodeId) {
- childrenIds.push(edge.target);
- }
- });
- setCollapsedNodes(prev => {
- const newSet = new Set(prev);
- // 展开所有祖先节点
- ancestorIds.forEach(id => newSet.delete(id));
- // 展开选中节点本身
- newSet.delete(nodeId);
- // 展开选中节点的直接子节点
- childrenIds.forEach(id => newSet.delete(id));
- return newSet;
- });
- // 延迟聚焦,等待节点展开和布局重新计算
- setTimeout(() => {
- fitView({
- nodes: [{ id: nodeId }],
- duration: 800,
- padding: 0.3,
- });
- }, 300);
- }}
- >
- {node.children && node.children.length > 0 && renderTree(node.children, level + 1)}
- </TreeNode>
- );
- });
- }, [collapsedTreeNodes, selectedNodeId, nodes, setCenter, initialEdges, setCollapsedNodes, fitView, mapTreeNodeToCanvasNode, initialNodes, setHiddenNodes]);
- console.log('📊 Rendering with', nodes.length, 'visible nodes and', edges.length, 'visible edges');
- if (nodes.length === 0) {
- return (
- <div style={{ padding: 50, color: 'red', fontSize: 20 }}>
- ERROR: No nodes to display!
- </div>
- );
- }
- return (
- <div style={{ width: '100vw', height: '100vh', background: '#f9fafb', display: 'flex', flexDirection: 'column' }}>
- {/* 顶部面包屑导航栏 */}
- <div style={{
- minHeight: '48px',
- maxHeight: '120px',
- background: 'white',
- borderBottom: '1px solid #e5e7eb',
- display: 'flex',
- alignItems: 'flex-start',
- padding: '12px 24px',
- zIndex: 1000,
- boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
- flexShrink: 0,
- overflowY: 'auto',
- }}>
- <div style={{ width: '100%' }}>
- {selectedNodeId ? (
- <div style={{ fontSize: '12px', color: '#6b7280' }}>
- {/* 面包屑导航 - 显示所有路径 */}
- {(() => {
- const selectedNode = nodes.find(n => n.id === selectedNodeId);
- if (!selectedNode) return null;
- // 找到所有从根节点到当前节点的路径
- const findAllPaths = (targetId) => {
- const paths = [];
- const buildPath = (nodeId, currentPath) => {
- const node = initialNodes.find(n => n.id === nodeId);
- if (!node) return;
- const newPath = [node, ...currentPath];
- // 找到所有父节点
- const parents = initialEdges.filter(e => e.target === nodeId).map(e => e.source);
- if (parents.length === 0) {
- // 到达根节点
- paths.push(newPath);
- } else {
- // 递归处理所有父节点
- parents.forEach(parentId => {
- buildPath(parentId, newPath);
- });
- }
- };
- buildPath(targetId, []);
- return paths;
- };
- const allPaths = findAllPaths(selectedNodeId);
- // 去重:将路径转换为字符串进行比较
- const uniquePaths = [];
- const pathStrings = new Set();
- allPaths.forEach(path => {
- const pathString = path.map(n => n.id).join('->');
- if (!pathStrings.has(pathString)) {
- pathStrings.add(pathString);
- uniquePaths.push(path);
- }
- });
- return (
- <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
- {uniquePaths.map((path, pathIndex) => (
- <div key={pathIndex} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
- {pathIndex > 0 && <span style={{ color: '#d1d5db', marginRight: '4px' }}>或</span>}
- {path.map((node, index) => {
- // 获取节点的 score、strategy 和 isSelected
- const nodeScore = node.data.score ? parseFloat(node.data.score) : 0;
- const nodeStrategy = getPrimaryStrategy(node.data); // 使用智能提取函数
- const strategyColor = getStrategyColor(nodeStrategy);
- const nodeIsSelected = node.type === 'note' ? node.data.matchLevel !== 'unsatisfied' : node.data.isSelected !== false;
- const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型
- // 计算路径节点字体颜色:根据分数提升幅度判断
- let pathFontColor = '#374151'; // 默认颜色
- if (node.type === 'note') {
- pathFontColor = node.data.matchLevel === 'unsatisfied' ? '#ef4444' : '#374151';
- } else if (node.data.seed_score !== undefined) {
- const parentScore = parseFloat(node.data.seed_score);
- const gain = nodeScore - parentScore;
- pathFontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
- } else if (index > 0) {
- const prevNode = path[index - 1];
- const prevScore = prevNode.data.score ? parseFloat(prevNode.data.score) : 0;
- const gain = nodeScore - prevScore;
- pathFontColor = gain >= 0.05 ? '#16a34a' : '#ef4444';
- } else if (node.data.isSelected === false) {
- pathFontColor = '#ef4444';
- }
- return (
- <React.Fragment key={node.id + '-' + index}>
- <span
- onClick={() => {
- const nodeId = node.id;
- // 找到所有祖先节点
- const ancestorIds = [nodeId];
- const findAncestors = (id) => {
- initialEdges.forEach(edge => {
- if (edge.target === id && !ancestorIds.includes(edge.source)) {
- ancestorIds.push(edge.source);
- findAncestors(edge.source);
- }
- });
- };
- findAncestors(nodeId);
- // 如果节点或其祖先被隐藏,先恢复它们
- setHiddenNodes(prev => {
- const newSet = new Set(prev);
- ancestorIds.forEach(id => newSet.delete(id));
- return newSet;
- });
- // 展开目录树中到达该节点的路径
- // 需要找到所有包含该节点的树路径的 treeKey,并展开它们的父节点
- setCollapsedTreeNodes(prev => {
- const newSet = new Set(prev);
- // 清空所有折叠状态,让目录树完全展开到选中节点
- // 这样可以确保选中节点在目录中可见
- return new Set();
- });
- setSelectedNodeId(nodeId);
- setTimeout(() => {
- fitView({
- nodes: [{ id: nodeId }],
- duration: 800,
- padding: 0.3,
- });
- }, 100);
- }}
- style={{
- padding: '6px 8px',
- borderRadius: '4px',
- background: 'white',
- border: index === path.length - 1 ? '2px solid #3b82f6' : '1px solid #d1d5db',
- color: '#374151',
- fontWeight: index === path.length - 1 ? '600' : '400',
- width: '180px',
- cursor: 'pointer',
- transition: 'all 0.2s ease',
- position: 'relative',
- display: 'inline-flex',
- flexDirection: 'column',
- gap: '4px',
- }}
- onMouseEnter={(e) => {
- e.currentTarget.style.opacity = '0.8';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.opacity = '1';
- }}
- title={\`\${node.data.title || node.id} (Score: \${nodeScore.toFixed(2)}, Strategy: \${nodeStrategy}, Selected: \${nodeIsSelected})\`}
- >
- {/* 上半部分:竖线 + 图标 + 文字 + 分数 */}
- <div style={{
- display: 'flex',
- alignItems: 'center',
- gap: '6px',
- }}>
- {/* 策略类型竖线 */}
- <div style={{
- width: '3px',
- height: '16px',
- background: strategyColor,
- borderRadius: '2px',
- flexShrink: 0,
- }} />
- {/* 节点文字 - 左侧 */}
- <span style={{
- flex: 1,
- fontSize: '12px',
- color: pathFontColor,
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- }}>
- {node.data.title || node.id}
- </span>
- {/* 域标识 - 右侧,挨着分数 */}
- {(node.data.domain_type || node.data.domains_str || (node.data.domain_index !== null && node.data.domain_index !== undefined)) && (
- <span style={{
- fontSize: '12px',
- color: '#fff',
- background: '#6366f1',
- padding: '2px 5px',
- borderRadius: '3px',
- flexShrink: 0,
- fontWeight: '600',
- marginLeft: '4px',
- }}
- title={
- node.data.domain_type ? '域: ' + node.data.domain_type + ' (D' + node.data.domain_index + ')' :
- node.data.domains_str ? '域: ' + node.data.domains_str :
- '域 D' + node.data.domain_index
- }
- >
- {node.data.domain_type || node.data.domains_str || ('D' + node.data.domain_index)}
- </span>
- )}
- {/* 分数显示 - 步骤和轮次节点不显示分数 */}
- {nodeActualType !== 'step' && nodeActualType !== 'round' && (
- <span style={{
- fontSize: '10px',
- color: '#6b7280',
- fontWeight: '500',
- flexShrink: 0,
- minWidth: '35px',
- textAlign: 'right',
- marginLeft: '4px',
- }}>
- {nodeScore.toFixed(2)}
- </span>
- )}
- </div>
- {/* 分数下划线 - 步骤和轮次节点不显示 */}
- {nodeActualType !== 'step' && nodeActualType !== 'round' && (
- <div style={{
- width: (nodeScore * 100) + '%',
- height: '2px',
- background: getScoreColor(nodeScore),
- borderRadius: '1px',
- marginLeft: '9px',
- }} />
- )}
- </span>
- {index < path.length - 1 && <span style={{ color: '#9ca3af' }}>›</span>}
- </React.Fragment>
- )})}
- </div>
- ))}
- </div>
- );
- })()}
- </div>
- ) : (
- <div style={{ fontSize: '13px', color: '#9ca3af', textAlign: 'center' }}>
- 选择一个节点查看路径
- </div>
- )}
- </div>
- </div>
- {/* 主内容区:目录 + 画布 */}
- <div style={{
- display: 'flex',
- flex: 1,
- overflow: 'hidden',
- cursor: isResizing ? 'col-resize' : 'default',
- userSelect: isResizing ? 'none' : 'auto',
- }}>
- {/* 左侧目录树 */}
- <div style={{
- width: \`\${sidebarWidth}px\`,
- background: 'white',
- borderRight: '1px solid #e5e7eb',
- display: 'flex',
- flexDirection: 'column',
- flexShrink: 0,
- }}>
- <div style={{
- padding: '12px 16px',
- borderBottom: '1px solid #e5e7eb',
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- }}>
- <span style={{
- fontWeight: '600',
- fontSize: '14px',
- color: '#111827',
- }}>
- 节点目录
- </span>
- <div style={{ display: 'flex', gap: '6px' }}>
- <button
- onClick={() => {
- setCollapsedTreeNodes(new Set());
- }}
- style={{
- fontSize: '11px',
- padding: '4px 8px',
- borderRadius: '4px',
- border: '1px solid #d1d5db',
- background: 'white',
- color: '#6b7280',
- cursor: 'pointer',
- fontWeight: '500',
- }}
- title="展开全部节点"
- >
- 全部展开
- </button>
- <button
- onClick={() => {
- const getAllTreeKeys = (nodes) => {
- const keys = new Set();
- const traverse = (node) => {
- if (node.children && node.children.length > 0) {
- keys.add(node.treeKey);
- node.children.forEach(traverse);
- }
- };
- nodes.forEach(traverse);
- return keys;
- };
- setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
- }}
- style={{
- fontSize: '11px',
- padding: '4px 8px',
- borderRadius: '4px',
- border: '1px solid #d1d5db',
- background: 'white',
- color: '#6b7280',
- cursor: 'pointer',
- fontWeight: '500',
- }}
- title="折叠全部节点"
- >
- 全部折叠
- </button>
- <button
- onClick={copyTreeToClipboard}
- style={{
- fontSize: '11px',
- padding: '4px 8px',
- borderRadius: '4px',
- border: '1px solid #3b82f6',
- background: '#3b82f6',
- color: 'white',
- cursor: 'pointer',
- fontWeight: '500',
- transition: 'all 0.2s',
- }}
- onMouseEnter={(e) => e.currentTarget.style.background = '#2563eb'}
- onMouseLeave={(e) => e.currentTarget.style.background = '#3b82f6'}
- title="复制树形结构为文本格式"
- >
- 📋 复制树形结构
- </button>
- </div>
- </div>
- <div style={{
- flex: 1,
- overflowX: 'auto',
- overflowY: 'auto',
- padding: '8px',
- }}>
- <div style={{ minWidth: 'fit-content' }}>
- {renderTree(treeRoots)}
- </div>
- </div>
- </div>
- {/* 可拖拽的分隔条 */}
- <div
- onMouseDown={handleMouseDown}
- style={{
- width: '4px',
- cursor: 'col-resize',
- background: isResizing ? '#3b82f6' : 'transparent',
- transition: isResizing ? 'none' : 'background 0.2s',
- flexShrink: 0,
- position: 'relative',
- }}
- onMouseEnter={(e) => e.currentTarget.style.background = '#e5e7eb'}
- onMouseLeave={(e) => {
- if (!isResizing) e.currentTarget.style.background = 'transparent';
- }}
- >
- {/* 拖拽提示线 */}
- <div style={{
- position: 'absolute',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: '1px',
- height: '40px',
- background: '#9ca3af',
- opacity: isResizing ? 1 : 0.3,
- }} />
- </div>
- {/* 画布区域 */}
- <div style={{ flex: 1, position: 'relative' }}>
- {/* 右侧图例 */}
- <div style={{
- position: 'absolute',
- top: '20px',
- right: '20px',
- background: 'white',
- padding: '16px',
- borderRadius: '12px',
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
- zIndex: 1000,
- maxWidth: '260px',
- border: '1px solid #e5e7eb',
- }}>
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
- <h3 style={{ fontSize: '14px', fontWeight: '600', color: '#111827', margin: 0 }}>图例</h3>
- <button
- onClick={() => setFocusMode(!focusMode)}
- style={{
- fontSize: '11px',
- padding: '4px 8px',
- borderRadius: '4px',
- border: '1px solid',
- borderColor: focusMode ? '#3b82f6' : '#d1d5db',
- background: focusMode ? '#3b82f6' : 'white',
- color: focusMode ? 'white' : '#6b7280',
- cursor: 'pointer',
- fontWeight: '500',
- }}
- title={focusMode ? '关闭聚焦模式' : '开启聚焦模式'}
- >
- {focusMode ? '🎯 聚焦' : '📊 全图'}
- </button>
- </div>
- <div style={{ fontSize: '12px' }}>
- {/* 画布节点展开/折叠控制 */}
- <div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #f3f4f6' }}>
- <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>节点控制</div>
- <div style={{ display: 'flex', gap: '6px' }}>
- <button
- onClick={() => {
- setCollapsedNodes(new Set());
- }}
- style={{
- fontSize: '11px',
- padding: '4px 8px',
- borderRadius: '4px',
- border: '1px solid #d1d5db',
- background: 'white',
- color: '#6b7280',
- cursor: 'pointer',
- fontWeight: '500',
- flex: 1,
- }}
- title="展开画布中所有节点的子节点"
- >
- 全部展开
- </button>
- <button
- onClick={() => {
- const allNodeIds = new Set(initialNodes.map(n => n.id));
- setCollapsedNodes(allNodeIds);
- }}
- style={{
- fontSize: '11px',
- padding: '4px 8px',
- borderRadius: '4px',
- border: '1px solid #d1d5db',
- background: 'white',
- color: '#6b7280',
- cursor: 'pointer',
- fontWeight: '500',
- flex: 1,
- }}
- title="折叠画布中所有节点的子节点"
- >
- 全部折叠
- </button>
- </div>
- </div>
- <div style={{ paddingTop: '12px', borderTop: '1px solid #f3f4f6' }}>
- <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>策略类型</div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#10b981', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>初始分词</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#06b6d4', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>调用sug</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#f59e0b', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>同义改写</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#3b82f6', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>加词</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#8b5cf6', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>抽象改写</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#ec4899', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>基于部分匹配改进</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#a855f7', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-抽象改写</span>
- </div>
- <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
- <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#fb923c', opacity: 0.7 }}></div>
- <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-同义改写</span>
- </div>
- </div>
- <div style={{
- marginTop: '12px',
- paddingTop: '12px',
- borderTop: '1px solid #f3f4f6',
- fontSize: '11px',
- color: '#9ca3af',
- lineHeight: '1.5',
- }}>
- 💡 点击节点左上角 × 隐藏节点
- </div>
- {/* 隐藏节点列表 - 在图例内部 */}
- {hiddenNodes.size > 0 && (
- <div style={{
- marginTop: '12px',
- paddingTop: '12px',
- borderTop: '1px solid #f3f4f6',
- }}>
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
- <h4 style={{ fontSize: '12px', fontWeight: '600', color: '#111827' }}>已隐藏节点</h4>
- <button
- onClick={() => setHiddenNodes(new Set())}
- style={{
- fontSize: '10px',
- color: '#3b82f6',
- background: 'none',
- border: 'none',
- cursor: 'pointer',
- textDecoration: 'underline',
- }}
- >
- 全部恢复
- </button>
- </div>
- <div style={{ fontSize: '12px', maxHeight: '200px', overflow: 'auto' }}>
- {Array.from(hiddenNodes).map(nodeId => {
- const node = initialNodes.find(n => n.id === nodeId);
- if (!node) return null;
- return (
- <div
- key={nodeId}
- style={{
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- padding: '6px 8px',
- margin: '4px 0',
- background: '#f9fafb',
- borderRadius: '6px',
- fontSize: '11px',
- }}
- >
- <span
- style={{
- flex: 1,
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- color: '#374151',
- }}
- title={node.data.title || nodeId}
- >
- {node.data.title || nodeId}
- </span>
- <button
- onClick={() => {
- setHiddenNodes(prev => {
- const newSet = new Set(prev);
- newSet.delete(nodeId);
- return newSet;
- });
- }}
- style={{
- marginLeft: '8px',
- fontSize: '10px',
- color: '#10b981',
- background: 'none',
- border: 'none',
- cursor: 'pointer',
- flexShrink: 0,
- }}
- >
- 恢复
- </button>
- </div>
- );
- })}
- </div>
- </div>
- )}
- </div>
- </div>
- {/* React Flow 画布 */}
- <ReactFlow
- nodes={nodes}
- edges={edges}
- nodeTypes={nodeTypes}
- fitView
- fitViewOptions={{ padding: 0.2, duration: 500 }}
- minZoom={0.4}
- maxZoom={1.5}
- nodesDraggable={true}
- nodesConnectable={false}
- elementsSelectable={true}
- defaultEdgeOptions={{
- type: 'smoothstep',
- }}
- proOptions={{ hideAttribution: true }}
- onNodeClick={(event, clickedNode) => {
- setSelectedNodeId(clickedNode.id);
- }}
- >
- <Controls style={{ bottom: '20px', left: 'auto', right: '20px' }} />
- <Background variant="dots" gap={20} size={1} color="#e5e7eb" />
- </ReactFlow>
- </div>
- </div>
- </div>
- );
- }
- function App() {
- return (
- <ReactFlowProvider>
- <FlowContent />
- </ReactFlowProvider>
- );
- }
- const root = createRoot(document.getElementById('root'));
- root.render(<App />);
- `;
- fs.writeFileSync(reactComponentPath, reactComponent);
- // 调试:保存临时组件副本用于检查
- fs.writeFileSync(path.join(__dirname, 'debug_component.jsx'), reactComponent);
- console.log('📝 已保存临时组件副本: debug_component.jsx');
- // 使用 esbuild 打包
- console.log('🎨 Building modern visualization...');
- build({
- entryPoints: [reactComponentPath],
- bundle: true,
- outfile: path.join(__dirname, 'bundle_v2.js'),
- format: 'iife',
- loader: {
- '.css': 'css',
- },
- minify: false,
- treeShaking: false, // 禁用tree shaking
- ignoreAnnotations: true, // 忽略所有注解,防止纯函数优化
- keepNames: true, // 保留函数和变量名
- sourcemap: 'inline',
- // 强制所有 React 引用指向同一个位置,避免多副本
- alias: {
- 'react': path.join(__dirname, 'node_modules/react'),
- 'react-dom': path.join(__dirname, 'node_modules/react-dom'),
- 'react/jsx-runtime': path.join(__dirname, 'node_modules/react/jsx-runtime'),
- 'react/jsx-dev-runtime': path.join(__dirname, 'node_modules/react/jsx-dev-runtime'),
- },
- define: {
- 'process.env.NODE_ENV': '"development"' // 使用开发模式,减少优化
- },
- }).then(() => {
- // 读取打包后的 JS
- const bundleJs = fs.readFileSync(path.join(__dirname, 'bundle_v2.js'), 'utf-8');
- // 调试:检查bundle中是否包含评估UI代码
- const hasEvalCode = bundleJs.includes('知识内容') || bundleJs.includes('is_knowledge');
- console.log('📝 Bundle调试: 包含评估代码 =', hasEvalCode);
- if (hasEvalCode) {
- console.log(' ✓ 评估UI代码在bundle中');
- } else {
- console.log(' ⚠️ 评估UI代码不在bundle中,检查临时组件文件...');
- const tempContent = fs.readFileSync(reactComponentPath, 'utf-8');
- const hasTempEvalCode = tempContent.includes('知识内容');
- console.log(' 临时组件文件包含评估代码 =', hasTempEvalCode);
- }
- // 读取 CSS
- const cssPath = path.join(__dirname, 'node_modules/@xyflow/react/dist/style.css');
- const css = fs.readFileSync(cssPath, 'utf-8');
- // 生成最终 HTML
- const html = `<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>查询图可视化</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
- <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
- <script>
- // 过滤特定的 React 警告
- const originalError = console.error;
- console.error = (...args) => {
- if (typeof args[0] === 'string' && args[0].includes('Each child in a list should have a unique "key" prop')) {
- return;
- }
- originalError.apply(console, args);
- };
- </script>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- overflow: hidden;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- #root {
- width: 100vw;
- height: 100vh;
- }
- ${css}
- /* 自定义样式覆盖 */
- .react-flow__edge-path {
- stroke-linecap: round;
- }
- .react-flow__controls {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- }
- .react-flow__controls-button {
- border: none;
- border-bottom: 1px solid #e5e7eb;
- }
- .react-flow__controls-button:hover {
- background: #f9fafb;
- }
- </style>
- </head>
- <body>
- <div id="root"></div>
- <script>${bundleJs}</script>
- </body>
- </html>`;
- // 调试:详细检查bundle和HTML内容
- const bundleHas知识 = bundleJs.includes('知识内容');
- const bundleHasIsKnowledge = bundleJs.includes('is_knowledge');
- const bundleHasDataIsKnowledge = bundleJs.includes('data.is_knowledge');
- console.log('📝 Bundle内容检查:');
- console.log(' 包含 "知识内容":', bundleHas知识);
- console.log(' 包含 "is_knowledge":', bundleHasIsKnowledge);
- console.log(' 包含 "data.is_knowledge":', bundleHasDataIsKnowledge);
- console.log(' Bundle长度:', bundleJs.length);
- const htmlHas知识 = html.includes('知识内容');
- const htmlHasIsKnowledge = html.includes('is_knowledge');
- const htmlHasDataIsKnowledge = html.includes('data.is_knowledge');
- console.log('📝 HTML内容检查:');
- console.log(' 包含 "知识内容":', htmlHas知识);
- console.log(' 包含 "is_knowledge":', htmlHasIsKnowledge);
- console.log(' 包含 "data.is_knowledge":', htmlHasDataIsKnowledge);
- console.log(' HTML长度:', html.length);
- // 如果bundle有但HTML没有,保存用于调试
- if ((bundleHas知识 || bundleHasDataIsKnowledge) && !htmlHas知识 && !htmlHasDataIsKnowledge) {
- console.log(' ⚠️ Bundle中有评估代码但HTML中没有!');
- fs.writeFileSync(path.join(__dirname, 'debug_bundle.js'), bundleJs);
- console.log(' 已保存 debug_bundle.js 用于调试');
- }
- // 写入输出文件
- fs.writeFileSync(outputFile, html);
- // 调试:暂时保留bundle文件用于分析
- console.log('📝 保留 bundle_v2.js 和 temp_flow_component_v2.jsx 用于调试');
- // 清理临时文件(调试期间注释掉)
- // fs.unlinkSync(reactComponentPath);
- // fs.unlinkSync(path.join(__dirname, 'bundle_v2.js'));
- console.log('✅ Visualization generated: ' + outputFile);
- console.log('📊 Nodes: ' + Object.keys(data.nodes).length);
- console.log('🔗 Edges: ' + data.edges.length);
- }).catch(error => {
- console.error('❌ Build error:', error);
- process.exit(1);
- });
|