creation_pattern.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>创作思维路径可视化</title>
  7. <script src="https://d3js.org/d3.v7.min.js"></script>
  8. <style>
  9. * {
  10. margin: 0;
  11. padding: 0;
  12. box-sizing: border-box;
  13. }
  14. body {
  15. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  16. background: #1a1a2e;
  17. color: #eee;
  18. overflow: hidden;
  19. }
  20. .container {
  21. display: flex;
  22. flex-direction: column;
  23. height: 100vh;
  24. }
  25. header {
  26. padding: 12px 20px;
  27. background: #16213e;
  28. border-bottom: 1px solid #0f3460;
  29. display: flex;
  30. align-items: center;
  31. gap: 20px;
  32. }
  33. h1 {
  34. font-size: 16px;
  35. font-weight: 500;
  36. color: #e94560;
  37. }
  38. .controls {
  39. display: flex;
  40. align-items: center;
  41. gap: 12px;
  42. flex: 1;
  43. }
  44. .slider-container {
  45. display: flex;
  46. align-items: center;
  47. gap: 8px;
  48. background: #0f3460;
  49. padding: 6px 12px;
  50. border-radius: 6px;
  51. }
  52. .slider-container label {
  53. font-size: 13px;
  54. color: #aaa;
  55. }
  56. #stepSlider {
  57. width: 200px;
  58. cursor: pointer;
  59. }
  60. #stepDisplay {
  61. font-size: 14px;
  62. font-weight: 600;
  63. color: #e94560;
  64. min-width: 60px;
  65. }
  66. .play-btn {
  67. background: #e94560;
  68. border: none;
  69. color: white;
  70. padding: 6px 14px;
  71. border-radius: 4px;
  72. cursor: pointer;
  73. font-size: 13px;
  74. }
  75. .play-btn:hover {
  76. background: #ff6b6b;
  77. }
  78. .legend {
  79. display: flex;
  80. gap: 16px;
  81. font-size: 12px;
  82. color: #888;
  83. margin-left: auto;
  84. }
  85. .legend-item {
  86. display: flex;
  87. align-items: center;
  88. gap: 4px;
  89. }
  90. .legend-dot {
  91. width: 10px;
  92. height: 10px;
  93. border-radius: 50%;
  94. }
  95. .legend-line {
  96. width: 20px;
  97. height: 2px;
  98. }
  99. main {
  100. flex: 1;
  101. position: relative;
  102. }
  103. svg {
  104. width: 100%;
  105. height: 100%;
  106. }
  107. .node {
  108. cursor: pointer;
  109. }
  110. .node circle {
  111. stroke-width: 2px;
  112. transition: all 0.3s;
  113. }
  114. .node.unknown circle {
  115. fill: #2a2a4a !important;
  116. stroke: #444 !important;
  117. opacity: 0.4;
  118. }
  119. .node.known circle {
  120. filter: drop-shadow(0 0 6px currentColor);
  121. }
  122. .node text {
  123. font-size: 11px;
  124. fill: #ccc;
  125. text-anchor: middle;
  126. pointer-events: none;
  127. }
  128. .node.unknown text {
  129. fill: #555;
  130. }
  131. .node .order-badge {
  132. font-size: 10px;
  133. font-weight: bold;
  134. fill: white;
  135. }
  136. .link {
  137. stroke-opacity: 0.6;
  138. transition: all 0.3s;
  139. }
  140. .link.hidden {
  141. stroke-opacity: 0 !important;
  142. }
  143. .link-label {
  144. font-size: 9px;
  145. fill: #888;
  146. pointer-events: none;
  147. }
  148. .link-label.hidden {
  149. opacity: 0;
  150. }
  151. /* 维度颜色 */
  152. .dim-灵感点 { --color: #ff6b6b; }
  153. .dim-目的点 { --color: #4ecdc4; }
  154. .dim-关键点 { --color: #ffe66d; }
  155. /* 信息面板 */
  156. .info-panel {
  157. position: absolute;
  158. top: 20px;
  159. right: 20px;
  160. width: 280px;
  161. background: #16213e;
  162. border: 1px solid #0f3460;
  163. border-radius: 8px;
  164. padding: 16px;
  165. font-size: 13px;
  166. max-height: calc(100vh - 120px);
  167. overflow-y: auto;
  168. }
  169. .info-panel h3 {
  170. color: #e94560;
  171. margin-bottom: 12px;
  172. font-size: 14px;
  173. }
  174. .info-item {
  175. margin-bottom: 8px;
  176. line-height: 1.5;
  177. }
  178. .info-label {
  179. color: #888;
  180. }
  181. .info-value {
  182. color: #eee;
  183. }
  184. .info-reason {
  185. background: #0f3460;
  186. padding: 8px;
  187. border-radius: 4px;
  188. margin-top: 8px;
  189. font-size: 12px;
  190. line-height: 1.6;
  191. }
  192. </style>
  193. </head>
  194. <body>
  195. <div class="container">
  196. <header>
  197. <h1>创作思维路径</h1>
  198. <div class="controls">
  199. <div class="slider-container">
  200. <label>步骤:</label>
  201. <input type="range" id="stepSlider" min="0" max="9" value="9">
  202. <span id="stepDisplay">9/9</span>
  203. </div>
  204. <button class="play-btn" id="playBtn">▶ 播放</button>
  205. </div>
  206. <div class="legend">
  207. <div class="legend-item">
  208. <span class="legend-dot" style="background: #ff6b6b;"></span>
  209. <span>灵感点</span>
  210. </div>
  211. <div class="legend-item">
  212. <span class="legend-dot" style="background: #4ecdc4;"></span>
  213. <span>目的点</span>
  214. </div>
  215. <div class="legend-item">
  216. <span class="legend-dot" style="background: #ffe66d;"></span>
  217. <span>关键点</span>
  218. </div>
  219. <div class="legend-item">
  220. <span class="legend-line" style="background: #e94560;"></span>
  221. <span>AI推导</span>
  222. </div>
  223. </div>
  224. </header>
  225. <main>
  226. <svg id="graph"></svg>
  227. <div class="info-panel" id="infoPanel" style="display: none;">
  228. <h3 id="infoTitle">节点详情</h3>
  229. <div id="infoContent"></div>
  230. </div>
  231. </main>
  232. </div>
  233. <script>
  234. // 创作模式数据(内联)
  235. const DATA = __CREATION_PATTERN_DATA__;
  236. // 维度颜色映射
  237. const dimColors = {
  238. '灵感点': '#ff6b6b',
  239. '目的点': '#4ecdc4',
  240. '关键点': '#ffe66d'
  241. };
  242. // 从数据中提取最终状态
  243. const steps = DATA['步骤列表'];
  244. const lastStep = steps[steps.length - 1];
  245. const nodes = lastStep['输出']['节点列表'];
  246. const edges = lastStep['输出']['边列表'].filter(e => e['关系类型'] === 'AI推导');
  247. const maxOrder = Math.max(...nodes.map(n => n['发现编号'] || 0));
  248. // 构建节点和边数据
  249. const nodeById = {};
  250. const graphNodes = nodes.map(n => {
  251. const node = {
  252. id: n['节点ID'],
  253. name: n['节点名称'],
  254. dimension: n['节点维度'],
  255. category: n['节点分类'],
  256. description: n['节点描述'],
  257. order: n['发现编号'],
  258. isKnown: n['是否已知']
  259. };
  260. nodeById[node.id] = node;
  261. return node;
  262. });
  263. const graphLinks = edges.map(e => ({
  264. source: e['来源'],
  265. target: e['目标'],
  266. type: e['关系类型'],
  267. score: e['可能性分数'],
  268. reason: e['推理说明']
  269. }));
  270. // 当前步骤
  271. let currentStep = maxOrder;
  272. const slider = document.getElementById('stepSlider');
  273. const stepDisplay = document.getElementById('stepDisplay');
  274. const playBtn = document.getElementById('playBtn');
  275. const infoPanel = document.getElementById('infoPanel');
  276. const infoTitle = document.getElementById('infoTitle');
  277. const infoContent = document.getElementById('infoContent');
  278. slider.max = maxOrder;
  279. slider.value = maxOrder;
  280. // 创建 SVG
  281. const svg = d3.select('#graph');
  282. const width = window.innerWidth;
  283. const height = window.innerHeight - 60;
  284. // 缩放
  285. const g = svg.append('g');
  286. const zoom = d3.zoom()
  287. .scaleExtent([0.2, 3])
  288. .on('zoom', (event) => g.attr('transform', event.transform));
  289. svg.call(zoom);
  290. // 箭头标记
  291. svg.append('defs').append('marker')
  292. .attr('id', 'arrow')
  293. .attr('viewBox', '0 -5 10 10')
  294. .attr('refX', 20)
  295. .attr('refY', 0)
  296. .attr('markerWidth', 6)
  297. .attr('markerHeight', 6)
  298. .attr('orient', 'auto')
  299. .append('path')
  300. .attr('d', 'M0,-5L10,0L0,5')
  301. .attr('fill', '#e94560');
  302. // 力导向图
  303. const simulation = d3.forceSimulation(graphNodes)
  304. .force('link', d3.forceLink(graphLinks).id(d => d.id).distance(120))
  305. .force('charge', d3.forceManyBody().strength(-400))
  306. .force('center', d3.forceCenter(width / 2, height / 2))
  307. .force('collision', d3.forceCollide().radius(50));
  308. // 绘制边
  309. const link = g.append('g')
  310. .selectAll('line')
  311. .data(graphLinks)
  312. .join('line')
  313. .attr('class', 'link')
  314. .attr('stroke', '#e94560')
  315. .attr('stroke-width', 2)
  316. .attr('marker-end', 'url(#arrow)');
  317. // 边标签
  318. const linkLabel = g.append('g')
  319. .selectAll('text')
  320. .data(graphLinks)
  321. .join('text')
  322. .attr('class', 'link-label')
  323. .text(d => d.score?.toFixed(2) || '');
  324. // 绘制节点
  325. const node = g.append('g')
  326. .selectAll('g')
  327. .data(graphNodes)
  328. .join('g')
  329. .attr('class', d => `node dim-${d.dimension}`)
  330. .call(d3.drag()
  331. .on('start', dragstarted)
  332. .on('drag', dragged)
  333. .on('end', dragended))
  334. .on('click', (event, d) => showNodeInfo(d))
  335. .on('mouseenter', (event, d) => {
  336. // 高亮相关边
  337. link.classed('highlighted', l => l.source.id === d.id || l.target.id === d.id);
  338. })
  339. .on('mouseleave', () => {
  340. link.classed('highlighted', false);
  341. });
  342. // 节点圆形
  343. node.append('circle')
  344. .attr('r', 18)
  345. .attr('fill', d => dimColors[d.dimension] || '#888')
  346. .attr('stroke', d => dimColors[d.dimension] || '#888');
  347. // 序号徽章
  348. node.append('text')
  349. .attr('class', 'order-badge')
  350. .attr('dy', 4)
  351. .text(d => d.order || '');
  352. // 节点名称
  353. node.append('text')
  354. .attr('dy', 35)
  355. .text(d => d.name.length > 6 ? d.name.slice(0, 6) + '…' : d.name);
  356. // Tick 更新
  357. simulation.on('tick', () => {
  358. link
  359. .attr('x1', d => d.source.x)
  360. .attr('y1', d => d.source.y)
  361. .attr('x2', d => d.target.x)
  362. .attr('y2', d => d.target.y);
  363. linkLabel
  364. .attr('x', d => (d.source.x + d.target.x) / 2)
  365. .attr('y', d => (d.source.y + d.target.y) / 2 - 8);
  366. node.attr('transform', d => `translate(${d.x},${d.y})`);
  367. });
  368. // 拖拽函数
  369. function dragstarted(event, d) {
  370. if (!event.active) simulation.alphaTarget(0.3).restart();
  371. d.fx = d.x;
  372. d.fy = d.y;
  373. }
  374. function dragged(event, d) {
  375. d.fx = event.x;
  376. d.fy = event.y;
  377. }
  378. function dragended(event, d) {
  379. if (!event.active) simulation.alphaTarget(0);
  380. d.fx = null;
  381. d.fy = null;
  382. }
  383. // 更新显示状态
  384. function updateDisplay(step) {
  385. currentStep = step;
  386. stepDisplay.textContent = `${step}/${maxOrder}`;
  387. // 更新节点状态
  388. node.classed('known', d => d.order && d.order <= step);
  389. node.classed('unknown', d => !d.order || d.order > step);
  390. // 更新边状态(源和目标都已知才显示)
  391. link.classed('hidden', d => {
  392. const srcKnown = nodeById[d.source.id || d.source]?.order <= step;
  393. const tgtKnown = nodeById[d.target.id || d.target]?.order <= step;
  394. return !srcKnown || !tgtKnown;
  395. });
  396. linkLabel.classed('hidden', d => {
  397. const srcKnown = nodeById[d.source.id || d.source]?.order <= step;
  398. const tgtKnown = nodeById[d.target.id || d.target]?.order <= step;
  399. return !srcKnown || !tgtKnown;
  400. });
  401. }
  402. // 滑块事件
  403. slider.addEventListener('input', (e) => {
  404. updateDisplay(parseInt(e.target.value));
  405. });
  406. // 播放功能
  407. let playing = false;
  408. let playInterval = null;
  409. playBtn.addEventListener('click', () => {
  410. if (playing) {
  411. clearInterval(playInterval);
  412. playBtn.textContent = '▶ 播放';
  413. playing = false;
  414. } else {
  415. if (currentStep >= maxOrder) {
  416. currentStep = 0;
  417. slider.value = 0;
  418. updateDisplay(0);
  419. }
  420. playing = true;
  421. playBtn.textContent = '⏸ 暂停';
  422. playInterval = setInterval(() => {
  423. currentStep++;
  424. slider.value = currentStep;
  425. updateDisplay(currentStep);
  426. if (currentStep >= maxOrder) {
  427. clearInterval(playInterval);
  428. playBtn.textContent = '▶ 播放';
  429. playing = false;
  430. }
  431. }, 800);
  432. }
  433. });
  434. // 显示节点信息
  435. function showNodeInfo(d) {
  436. infoPanel.style.display = 'block';
  437. infoTitle.textContent = d.name;
  438. // 找到指向这个节点的边
  439. const inEdge = graphLinks.find(e => (e.target.id || e.target) === d.id);
  440. let html = `
  441. <div class="info-item">
  442. <span class="info-label">维度:</span>
  443. <span class="info-value">${d.dimension}</span>
  444. </div>
  445. <div class="info-item">
  446. <span class="info-label">分类:</span>
  447. <span class="info-value">${d.category || '-'}</span>
  448. </div>
  449. <div class="info-item">
  450. <span class="info-label">发现顺序:</span>
  451. <span class="info-value">${d.order || '未知'}</span>
  452. </div>
  453. <div class="info-item">
  454. <span class="info-label">描述:</span>
  455. <div class="info-reason">${d.description || '-'}</div>
  456. </div>
  457. `;
  458. if (inEdge) {
  459. const srcNode = nodeById[inEdge.source.id || inEdge.source];
  460. html += `
  461. <div class="info-item" style="margin-top: 12px;">
  462. <span class="info-label">推导来源:</span>
  463. <span class="info-value">${srcNode?.name || '-'}</span>
  464. </div>
  465. <div class="info-item">
  466. <span class="info-label">可能性:</span>
  467. <span class="info-value">${(inEdge.score * 100).toFixed(0)}%</span>
  468. </div>
  469. <div class="info-item">
  470. <span class="info-label">推理说明:</span>
  471. <div class="info-reason">${inEdge.reason || '-'}</div>
  472. </div>
  473. `;
  474. }
  475. infoContent.innerHTML = html;
  476. }
  477. // 点击空白关闭信息面板
  478. svg.on('click', (event) => {
  479. if (event.target === svg.node()) {
  480. infoPanel.style.display = 'none';
  481. }
  482. });
  483. // 初始显示
  484. updateDisplay(maxOrder);
  485. // 居中显示
  486. setTimeout(() => {
  487. const bounds = g.node().getBBox();
  488. const scale = Math.min(
  489. (width - 100) / bounds.width,
  490. (height - 100) / bounds.height,
  491. 1.2
  492. );
  493. const tx = (width - bounds.width * scale) / 2 - bounds.x * scale;
  494. const ty = (height - bounds.height * scale) / 2 - bounds.y * scale;
  495. svg.transition().duration(500).call(
  496. zoom.transform,
  497. d3.zoomIdentity.translate(tx, ty).scale(scale)
  498. );
  499. }, 1000);
  500. </script>
  501. </body>
  502. </html>