| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182 |
- const STATUS_LABELS = {
- pending: "待开始",
- "in-progress": "执行中",
- completed: "已完成",
- bug: "Bug",
- };
- function svgEl(tagName, attrs = {}) {
- const node = document.createElementNS("http://www.w3.org/2000/svg", tagName);
- Object.entries(attrs).forEach(([key, value]) => {
- if (value !== undefined && value !== null) node.setAttribute(key, String(value));
- });
- return node;
- }
- function truncate(text, max = 26) {
- if (!text) return "";
- if (text.length <= max) return text;
- return `${text.slice(0, max - 1)}…`;
- }
- function oneLine(text) {
- return String(text || "").replace(/\s+/g, " ").trim();
- }
- function toPrettyTime(isoValue) {
- if (!isoValue) return "-";
- const date = new Date(isoValue);
- if (Number.isNaN(date.getTime())) return "-";
- const y = date.getFullYear();
- const m = String(date.getMonth() + 1).padStart(2, "0");
- const d = String(date.getDate()).padStart(2, "0");
- const h = String(date.getHours()).padStart(2, "0");
- const min = String(date.getMinutes()).padStart(2, "0");
- return `${y}-${m}-${d} ${h}:${min}`;
- }
- function edgePath(from, to) {
- const c1x = from.x + 92;
- const c2x = to.x - 92;
- return `M ${from.x} ${from.y} C ${c1x} ${from.y}, ${c2x} ${to.y}, ${to.x} ${to.y}`;
- }
- export class ThoughtChainRenderer {
- constructor(params) {
- this.svg = params.svg;
- this.linksLayer = params.linksLayer;
- this.nodesLayer = params.nodesLayer;
- }
- renderEmpty() {
- this.linksLayer.innerHTML = "";
- this.nodesLayer.innerHTML = "";
- const text = svgEl("text", {
- x: 180,
- y: 200,
- class: "empty-state",
- });
- text.textContent = "当前会话暂无节点";
- this.nodesLayer.appendChild(text);
- }
- render(graph, layout, options = {}) {
- const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
- if (!nodes.length) {
- this.renderEmpty();
- return;
- }
- const selectedNodeId = options.selectedNodeId || null;
- const onNodeClick = typeof options.onNodeClick === "function" ? options.onNodeClick : null;
- this.linksLayer.innerHTML = "";
- this.nodesLayer.innerHTML = "";
- for (const link of layout.links) {
- const path = svgEl("path", {
- class: `link-path status-${link.status} type-${link.type || "reference"}`,
- d: edgePath(link.from, link.to),
- });
- this.linksLayer.appendChild(path);
- }
- for (const node of nodes) {
- const pos = layout.positions.get(node.id);
- if (!pos) continue;
- const selectedClass = node.id === selectedNodeId ? " selected" : "";
- const group = svgEl("g", {
- class: `node-group status-${node.status}${selectedClass}`,
- transform: `translate(${pos.x} ${pos.y})`,
- "data-id": node.id,
- });
- if (node.status === "in-progress") {
- const ring = svgEl("rect", {
- class: "progress-ring",
- x: -146,
- y: -58,
- width: 292,
- height: 116,
- rx: 24,
- });
- group.appendChild(ring);
- }
- const card = svgEl("rect", {
- class: "node-card",
- x: -135,
- y: -50,
- width: 270,
- height: 100,
- rx: 18,
- });
- group.appendChild(card);
- const statusDot = svgEl("circle", {
- class: "node-status-dot",
- cx: -114,
- cy: -31,
- r: 7,
- });
- group.appendChild(statusDot);
- const title = svgEl("text", {
- class: "node-title",
- x: -98,
- y: -27,
- });
- title.textContent = truncate(oneLine(node.name), 22);
- group.appendChild(title);
- const line1 = svgEl("text", {
- class: "node-subtitle",
- x: -98,
- y: -5,
- });
- line1.textContent = `${STATUS_LABELS[node.status] || node.status} · ${node.nodeType}`;
- group.appendChild(line1);
- const line2 = svgEl("text", {
- class: "node-subtitle",
- x: -98,
- y: 16,
- });
- line2.textContent = toPrettyTime(node.timestamps?.created || node.createdAt);
- group.appendChild(line2);
- const line3 = svgEl("text", {
- class: "node-subtitle muted",
- x: -98,
- y: 36,
- });
- line3.textContent = truncate(oneLine(node.content || node.notes || ""), 28) || "无详细内容";
- group.appendChild(line3);
- if (onNodeClick) {
- group.addEventListener("click", (event) => {
- event.stopPropagation();
- onNodeClick(node.id);
- });
- }
- this.nodesLayer.appendChild(group);
- }
- }
- exportSvg(filename) {
- const cloned = this.svg.cloneNode(true);
- cloned.setAttribute("xmlns", "http://www.w3.org/2000/svg");
- cloned.setAttribute("width", String(this.svg.clientWidth || 1600));
- cloned.setAttribute("height", String(this.svg.clientHeight || 960));
- const source = new XMLSerializer().serializeToString(cloned);
- const blob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = filename;
- link.click();
- URL.revokeObjectURL(url);
- }
- }
|