<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HarmonyOS 知识图谱</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI','PingFang SC',sans-serif;overflow:hidden;background:#0f1923}
#app{display:flex;height:100vh}
#sidebar{width:320px;background:#1a2733;color:#ccc;display:flex;flex-direction:column;border-right:1px solid #2a3a4a}
#sidebar h1{font-size:18px;padding:16px;background:#111d28;color:#fff;border-bottom:1px solid #2a3a4a}
#search{padding:10px 16px;border-bottom:1px solid #2a3a4a}
#search input{width:100%;padding:8px 12px;border:1px solid #3a4a5a;border-radius:6px;background:#0d1820;color:#fff;font-size:13px;outline:none}
#search input:focus{border-color:#5b9bd5}
#filters{padding:8px 16px;display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid #2a3a4a}
#filters button{padding:4px 10px;border:1px solid #3a4a5a;border-radius:14px;background:transparent;color:#aaa;font-size:12px;cursor:pointer;transition:.15s}
#filters button:hover{background:#2a3a4a;color:#fff}
#filters button.active{background:#5b9bd5;border-color:#5b9bd5;color:#fff}
#stats{padding:10px 16px;font-size:12px;color:#789;border-bottom:1px solid #2a3a4a}
#node-list{flex:1;overflow-y:auto;padding:8px 0}
#node-list .item{padding:8px 16px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:.1s;font-size:13px}
#node-list .item:hover{background:#1e3344}
#node-list .item.selected{background:#5b9bd520;border-left:3px solid #5b9bd5}
#node-list .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
#detail{width:340px;background:#1a2733;color:#ccc;display:none;flex-direction:column;border-left:1px solid #2a3a4a;overflow-y:auto}
#detail.active{display:flex}
#detail h2{font-size:16px;padding:16px;background:#111d28;color:#fff;display:flex;justify-content:space-between;align-items:center}
#detail h2 button{background:none;border:none;color:#789;font-size:20px;cursor:pointer}
#detail h2 button:hover{color:#fff}
#detail .body{padding:16px;flex:1}
#detail .field{margin-bottom:12px}
#detail .label{font-size:11px;color:#789;text-transform:uppercase;margin-bottom:2px}
#detail .value{font-size:13px;color:#ddd;word-break:break-all}
#detail .neighbors{margin-top:8px}
#detail .neighbor{padding:6px 10px;margin:2px 0;border-radius:4px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px}
#detail .neighbor:hover{background:#1e3344}
#graph{flex:1;position:relative}
#graph svg{width:100%;height:100%}
#tooltip{position:absolute;padding:8px 12px;background:#1a2733;border:1px solid #3a4a5a;border-radius:6px;color:#ddd;font-size:12px;pointer-events:none;display:none;max-width:300px;z-index:10}
#loading{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#789;font-size:14px}
.legend{position:absolute;bottom:16px;left:16px;display:flex;gap:12px;font-size:11px;color:#789}
.legend span{display:flex;align-items:center;gap:4px}
.legend .ldot{width:8px;height:8px;border-radius:50%}
</style>
</head>
<body>
<div id="app">
  <div id="sidebar">
    <h1>🕸 HarmonyOS 知识图谱</h1>
    <div id="search"><input type="text" placeholder="搜索节点…" id="searchInput"></div>
    <div id="filters">
      <button data-type="" class="active">全部</button>
      <button data-type="kit">📦 Kit</button>
      <button data-type="module">📡 模块</button>
      <button data-type="component">🧩 组件</button>
      <button data-type="guide">📖 指南</button>
    </div>
    <div id="stats"></div>
    <div id="node-list"></div>
  </div>
  <div id="graph">
    <div id="loading">加载图谱数据…</div>
    <div id="tooltip"></div>
    <div class="legend">
      <span><span class="ldot" style="background:#5b9bd5"></span>Kit</span>
      <span><span class="ldot" style="background:#4caf50"></span>模块</span>
      <span><span class="ldot" style="background:#ff9800"></span>组件</span>
      <span><span class="ldot" style="background:#888"></span>指南</span>
    </div>
  </div>
  <div id="detail">
    <h2><span id="detailTitle">节点详情</span><button onclick="closeDetail()"></button></h2>
    <div class="body" id="detailBody"></div>
  </div>
</div>

<script>
const TYPE_COLORS = { kit:'#5b9bd5', module:'#4caf50', component:'#ff9800', guide:'#888', error_code:'#e74c3c' };
const TYPE_EMOJI = { kit:'📦', module:'📡', component:'🧩', guide:'📖', error_code:'⚠️' };
const RELATION_LABEL = { belongs_to:'属于', contains:'包含', references:'引用', related:'关联' };

let allNodes = [], allEdges = [];
let currentFilter = '';
let selectedNodeId = null;

// D3 simulation
const width = () => document.getElementById('graph').clientWidth;
const height = () => document.getElementById('graph').clientHeight;

const svg = d3.select('#graph').append('svg');
const g = svg.append('g');
const linkG = g.append('g').attr('class', 'links');
const nodeG = g.append('g').attr('class', 'nodes');

const simulation = d3.forceSimulation()
  .force('link', d3.forceLink().id(d => d.id).distance(80))
  .force('charge', d3.forceManyBody().strength(-200))
  .force('center', d3.forceCenter(width()/2, height()/2))
  .force('collision', d3.forceCollide(25));

const zoom = d3.zoom().scaleExtent([0.1, 4]).on('zoom', e => g.attr('transform', e.transform));
svg.call(zoom);

// Tooltip
const tooltip = d3.select('#tooltip');

// Load data
async function loadGraph(query = '') {
  document.getElementById('loading').style.display = 'block';
  const url = query ? `/api/graph?q=${encodeURIComponent(query)}&limit=300` : '/api/graph?limit=400';
  const data = await fetch(url).then(r => r.json());
  allNodes = data.nodes || [];
  allEdges = data.edges || [];
  document.getElementById('loading').style.display = 'none';
  updateStats();
  updateList();
  renderGraph();
}

function updateStats() {
  const byType = {};
  for (const n of allNodes) { byType[n.type] = (byType[n.type]||0)+1; }
  const parts = Object.entries(byType).map(([t,c]) => `${TYPE_EMOJI[t]||''} ${c}`).join(' · ');
  document.getElementById('stats').textContent = `节点 ${allNodes.length} · 边 ${allEdges.length} · ${parts}`;
}

function updateList(filter = '') {
  const list = document.getElementById('node-list');
  let nodes = allNodes;
  if (filter) nodes = nodes.filter(n => n.type === filter);
  // 按类型分组
  const order = ['kit','module','component','guide'];
  nodes = [...nodes].sort((a,b) => order.indexOf(a.type)-order.indexOf(b.type) || a.name.localeCompare(b.name));
  const maxShow = 100;
  const shown = nodes.slice(0, maxShow);
  list.innerHTML = shown.map(n => `
    <div class="item${selectedNodeId===n.id?' selected':''}" data-id="${n.id}" onclick="selectNode('${n.id.replace(/'/g,"\\'")}')">
      <span class="dot" style="background:${TYPE_COLORS[n.type]}"></span>
      <span>${TYPE_EMOJI[n.type]||''} ${n.name}</span>
    </div>
  `).join('');
  if (nodes.length > maxShow) {
    list.innerHTML += `<div style="padding:8px 16px;color:#789;font-size:12px">还有 ${nodes.length-maxShow} 个节点…</div>`;
  }
}

function renderGraph() {
  // Filter to visible nodes
  let visible = allNodes;
  if (currentFilter) visible = allNodes.filter(n => n.type === currentFilter);
  const visibleIds = new Set(visible.map(n => n.id));
  const visibleEdges = allEdges.filter(e => visibleIds.has(e.from) && visibleIds.has(e.to));

  // Update simulation
  const oldNodes = new Map(simulation.nodes().map(n => [n.id, n]));
  const nodes = visible.map(n => {
    const old = oldNodes.get(n.id);
    return old ? Object.assign(old, n) : n;
  });

  simulation.nodes(nodes);
  simulation.force('link').links(visibleEdges);
  simulation.alpha(1).restart();

  // Links
  const link = linkG.selectAll('line').data(visibleEdges, d => `${d.from}-${d.to}-${d.relation}`);
  link.exit().remove();
  link.enter().append('line')
    .attr('stroke', d => d.relation==='contains'?'#3a5a7a':d.relation==='belongs_to'?'#4a6a3a':'#5a4a3a')
    .attr('stroke-width', d => d.relation==='contains'?1.5:1)
    .attr('stroke-dasharray', d => d.relation==='references'?'4,3':null)
    .attr('opacity', 0.4);

  // Nodes
  const node = nodeG.selectAll('g.node').data(nodes, d => d.id);

  const nodeEnter = node.enter().append('g').attr('class', 'node')
    .call(d3.drag().on('start',(e,d)=>{if(!e.active)simulation.alphaTarget(0.3).restart();d.fx=d.x;d.fy=d.y})
      .on('drag',(e,d)=>{d.fx=e.x;d.fy=e.y})
      .on('end',(e,d)=>{if(!e.active)simulation.alphaTarget(0);d.fx=null;d.fy=null}))
    .on('click', (e,d) => { e.stopPropagation(); selectNode(d.id); })
    .on('mouseenter', (e,d) => {
      tooltip.style('display','block')
        .html(`<b>${TYPE_EMOJI[d.type]||''} ${d.name}</b><br><span style="color:#789">${d.type}</span>${d.kit?`<br>Kit: ${d.kit}`:''}`);
    })
    .on('mousemove', e => { tooltip.style('left',(e.pageX-280)+'px').style('top',(e.pageY-40)+'px'); })
    .on('mouseleave', () => tooltip.style('display','none'));

  nodeEnter.append('circle')
    .attr('r', d => d.type==='kit'?12:d.type==='module'?7:d.type==='component'?6:5)
    .attr('fill', d => TYPE_COLORS[d.type]);
  nodeEnter.append('text')
    .text(d => d.name.length > 10 ? d.name.slice(0,10)+'…' : d.name)
    .attr('dy', d => d.type==='kit'?20:14)
    .attr('text-anchor','middle')
    .attr('fill','#aaa')
    .attr('font-size', d => d.type==='kit'?10:8);

  node.exit().remove();

  simulation.on('tick', () => {
    link.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
    node.attr('transform', d => `translate(${d.x},${d.y})`);
  });
}

async function selectNode(id) {
  selectedNodeId = id;
  updateList(currentFilter);
  // Highlight in graph
  nodeG.selectAll('circle').attr('stroke', d => d.id===id?'#fff':null).attr('stroke-width', d => d.id===id?2:0);

  // Load details
  const data = await fetch(`/api/graph/node/${encodeURIComponent(id)}`).then(r => r.json());
  if (!data.node) return;

  document.getElementById('detail').classList.add('active');
  document.getElementById('detailTitle').textContent = data.node.name;

  const n = data.node;
  let html = `
    <div class="field"><div class="label">类型</div><div class="value">${TYPE_EMOJI[n.type]||''} ${n.type}</div></div>
    <div class="field"><div class="label">ID</div><div class="value" style="font-size:11px;color:#789">${n.id}</div></div>
    ${n.kit?`<div class="field"><div class="label">所属 Kit</div><div class="value">${n.kit}</div></div>`:''}
    ${n.path?`<div class="field"><div class="label">文档路径</div><div class="value" style="font-size:11px">${n.path}</div></div>`:''}
    <div class="field"><div class="label">关联节点 (${data.edges.length})</div></div>
  `;

  for (const e of data.edges.slice(0, 20)) {
    const neighborId = e.from === id ? e.to : e.from;
    const rel = RELATION_LABEL[e.relation] || e.relation;
    html += `<div class="neighbor" onclick="selectNode('${neighborId.replace(/'/g,"\\'")}')">
      <span class="dot" style="background:${TYPE_COLORS[n.type]}"></span>
      ${rel} → <span style="color:#5b9bd5">${neighborId}</span>
    </div>`;
  }
  if (data.edges.length > 20) {
    html += `<div style="color:#789;font-size:12px;padding:6px">还有 ${data.edges.length-20} 个关联…</div>`;
  }
  document.getElementById('detailBody').innerHTML = html;
}

function closeDetail() {
  document.getElementById('detail').classList.remove('active');
  selectedNodeId = null;
  nodeG.selectAll('circle').attr('stroke',null).attr('stroke-width',0);
}

// Search
let searchTimer;
document.getElementById('searchInput').addEventListener('input', function() {
  clearTimeout(searchTimer);
  searchTimer = setTimeout(() => {
    loadGraph(this.value);
  }, 300);
});

// Filters
document.querySelectorAll('#filters button').forEach(btn => {
  btn.addEventListener('click', function() {
    document.querySelectorAll('#filters button').forEach(b => b.classList.remove('active'));
    this.classList.add('active');
    currentFilter = this.dataset.type;
    updateList(currentFilter);
    renderGraph();
  });
});

// Click background to close detail
svg.on('click', () => { closeDetail(); });

// Resize
window.addEventListener('resize', () => {
  simulation.force('center', d3.forceCenter(width()/2, height()/2));
  simulation.alpha(0.1).restart();
});

// Start
loadGraph();
</script>
</body>
</html>