chore(repo): initialize Cowork Local Gitea repository
CI / test (push) Canceled after 0s

This commit is contained in:
thanhnv
2026-08-09 20:12:05 +07:00
commit 414eaddca3
192 changed files with 48160 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
Đặt biểu tượng ứng dụng tại đây (icon.png / icon.ico) nếu muốn tùy biến.
Thư mục này được giữ lại để build_exe.bat đóng gói cùng ứng dụng.
+1284
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
<!--
RULEforCode.md — security rules for the CODE agent ONLY.
Intentionally left empty for now (to be defined later). The Code agent does NOT
go through RULEBASE.md (that rulebase — including any "no coding" restriction —
applies to the Cowork agent only). The Code agent's safety comes from running
inside the sandbox (resource limits, network policy, command whitelist), not
from this rulebase yet.
Add code-specific rules below when they are decided; they will be injected into
the Code agent's system prompt and checked by the active guardrail, exactly the
way RULEBASE.md is for Cowork.
-->
+2
View File
File diff suppressed because one or more lines are too long
+799
View File
@@ -0,0 +1,799 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ABX-PDS — Knowledge Graph</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<style>
/* Palette mirrors the desktop app's theme (src/cowork_local/theme.py) so the
embedded graph reads as part of the same product, not a separate tool. */
:root {
--bg: #131317;
--surface: #191a1f;
--surface-raised:#1c1d23;
--border: #232430;
--border-strong: #33343d;
--text: #eceef2;
--text-muted: #8b8d98;
--accent: #1e90ff;
--accent2: #00bfff;
--gradient: linear-gradient(90deg, var(--accent2), var(--accent));
--c-org: #4ECDC4;
--c-person: #FF6B6B;
--c-leg: #60a5fa;
--c-case: #FBBF24;
--c-concept: #86efac;
--c-gov: #c084fc;
--c-ai: #1e90ff;
--c-other: #94a3b8;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
::selection { background: var(--accent); color: white; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent); }
body {
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", "Yu Gothic UI", "Meiryo", sans-serif;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Header ─────────────────────────────────────────────────── */
header {
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: var(--surface);
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
header h1 {
font-size: 14px;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.02em;
}
.stats {
margin-left: auto;
font-size: 11px;
color: var(--text-muted);
display: flex;
gap: 16px;
}
.stats span { color: var(--text); font-weight: 600; }
/* ── Layout ──────────────────────────────────────────────────── */
.layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Sidebar ─────────────────────────────────────────────────── */
.sidebar {
width: 220px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow-y: auto;
}
.sidebar-section {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.sidebar-label {
font-size: 10px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 10px;
}
/* ── Legend items ────────────────────────────────────────────── */
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
user-select: none;
}
.legend-item:hover { background: var(--surface-raised); }
.legend-item.disabled { opacity: 0.3; }
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-name {
font-size: 12px;
color: var(--text);
flex: 1;
}
.legend-count {
font-size: 10px;
color: var(--text-muted);
}
/* ── Search ──────────────────────────────────────────────────── */
.search-wrap { padding: 12px 16px; border-bottom: 1px solid var(--border); }
input[type="search"] {
width: 100%;
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: 10px;
padding: 7px 10px;
font-family: inherit;
font-size: 12px;
color: var(--text);
outline: none;
transition: border-color 0.15s;
}
input[type="search"]:focus { border-color: var(--accent); }
input[type="search"]::placeholder { color: var(--text-muted); }
/* ── Controls ────────────────────────────────────────────────── */
.controls { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
.control-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.control-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
input[type="range"] {
width: 100%;
accent-color: var(--accent);
cursor: pointer;
}
.btn-row { display: flex; gap: 6px; margin-top: 4px; }
button {
flex: 1;
padding: 7px 8px;
border-radius: 10px;
border: 1px solid var(--border-strong);
background: var(--surface-raised);
color: var(--text);
font-family: inherit;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
button:hover { border-color: var(--accent); background: #26272f; }
button.primary {
background: var(--gradient);
border: none;
color: white;
}
button.primary:hover { filter: brightness(1.08); }
/* ── Canvas ──────────────────────────────────────────────────── */
#graph-container {
flex: 1;
position: relative;
overflow: hidden;
background: var(--bg);
}
svg { width: 100%; height: 100%; }
/* ── Links ───────────────────────────────────────────────────── */
.link {
stroke: var(--border-strong);
stroke-opacity: 0.7;
stroke-width: 1.5px;
transition: stroke 0.2s, stroke-opacity 0.2s;
}
.link.highlighted { stroke: var(--accent); stroke-opacity: 1; stroke-width: 2px; }
.link.dimmed { stroke-opacity: 0.08; }
/* ── Edge labels ─────────────────────────────────────────────── */
.edge-label {
font-size: 8px;
fill: var(--text-muted);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
}
.edge-label.visible { opacity: 1; }
/* ── Nodes ───────────────────────────────────────────────────── */
.node circle {
stroke: rgba(255,255,255,0.15);
stroke-width: 1.5px;
cursor: pointer;
transition: stroke 0.15s, stroke-width 0.15s, filter 0.15s;
}
.node circle:hover {
stroke: white;
stroke-width: 2.5px;
filter: brightness(1.3);
}
.node.selected circle { stroke: white; stroke-width: 3px; filter: brightness(1.4); }
.node.dimmed circle { opacity: 0.12; }
.node.dimmed text { opacity: 0.05; }
.node text {
font-family: inherit;
font-size: 11px;
fill: var(--text);
pointer-events: none;
text-anchor: middle;
dominant-baseline: middle;
transition: opacity 0.2s;
}
/* ── Tooltip ─────────────────────────────────────────────────── */
#tooltip {
position: absolute;
background: var(--surface-raised);
border: 1px solid var(--border-strong);
border-radius: 12px;
padding: 10px 13px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
max-width: 260px;
z-index: 10;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
#tooltip.visible { opacity: 1; }
.tt-name {
font-weight: 700;
font-size: 13px;
margin-bottom: 3px;
color: var(--text);
}
.tt-type {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 6px;
}
.tt-desc {
font-size: 11px;
color: var(--text-muted);
line-height: 1.5;
margin-bottom: 6px;
}
.tt-connections {
font-size: 10px;
color: var(--text-muted);
border-top: 1px solid var(--border);
padding-top: 6px;
margin-top: 4px;
}
.tt-connections span { color: var(--accent); font-weight: 700; }
</style>
</head>
<body>
<header>
<h1>Knowledge Graph</h1>
<div class="stats">
nodes <span id="stat-nodes">—</span> &nbsp;|&nbsp;
edges <span id="stat-edges">—</span> &nbsp;|&nbsp;
communities <span id="stat-communities">—</span>
</div>
</header>
<div class="layout">
<!-- ── Sidebar ─────────────────────────────────────── -->
<aside class="sidebar">
<div class="search-wrap">
<input type="search" id="search" placeholder="Search nodes…">
</div>
<div class="sidebar-section">
<div class="sidebar-label">Entity Types</div>
<div id="legend"></div>
</div>
<div class="sidebar-section">
<div class="sidebar-label">Controls</div>
<div class="controls">
<div class="control-row">
<div class="control-label">Link distance</div>
<input type="range" id="link-distance" min="40" max="300" value="120">
</div>
<div class="control-row">
<div class="control-label">Charge strength</div>
<input type="range" id="charge" min="-800" max="-50" value="-300">
</div>
<div class="control-row">
<label class="control-label" style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" id="show-rel" checked>
Show relationship
</label>
</div>
<div class="btn-row">
<button id="btn-reset" class="primary">Reset zoom</button>
<button id="btn-all">Show all</button>
</div>
</div>
</div>
</aside>
<!-- ── Graph canvas ────────────────────────────────── -->
<div id="graph-container">
<svg id="graph">
<defs>
<marker id="arrow" viewBox="0 -4 10 8" refX="18" refY="0"
markerWidth="6" markerHeight="6" orient="auto">
<path d="M0,-4L10,0L0,4" fill="#33343d"/>
</marker>
</defs>
<g id="root"></g>
</svg>
<div id="tooltip"></div>
</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════════════════════
// DATA — injected by Python (replace GRAPH_DATA_PLACEHOLDER with real data)
// ═══════════════════════════════════════════════════════════════════════════
const GRAPH_DATA = GRAPH_DATA_PLACEHOLDER;
// ═══════════════════════════════════════════════════════════════════════════
// COLOUR MAP
// ═══════════════════════════════════════════════════════════════════════════
const COLOR_MAP = {
ORGANIZATION: "#4ECDC4",
PERSON: "#FF6B6B",
LEGISLATION: "#60a5fa",
LEGAL_CASE: "#FBBF24",
CONCEPT: "#86efac",
GOVERNMENT: "#c084fc",
AI_SYSTEM: "#1e90ff",
OTHER: "#94a3b8",
};
function nodeColor(type) {
return COLOR_MAP[type] || COLOR_MAP.OTHER;
}
// ═══════════════════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════════════════
const svg = d3.select("#graph");
const root = d3.select("#root");
const tooltip = document.getElementById("tooltip");
const container = document.getElementById("graph-container");
let W = container.clientWidth;
let H = container.clientHeight;
// Zoom behaviour (composed with a gentle idle rotation, see bottom of file)
let _zt = d3.zoomIdentity;
function applyTransform() {
root.attr("transform", `${_zt}`);
}
const zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on("zoom", e => { _zt = e.transform; applyTransform(); });
svg.call(zoom);
// ═══════════════════════════════════════════════════════════════════════════
// FORCE SIMULATION
// ═══════════════════════════════════════════════════════════════════════════
const nodes = GRAPH_DATA.nodes.map(d => ({ ...d }));
const links = GRAPH_DATA.links.map(d => ({ ...d }));
// Compute degree for node sizing
const degreeMap = {};
links.forEach(l => {
degreeMap[l.source] = (degreeMap[l.source] || 0) + 1;
degreeMap[l.target] = (degreeMap[l.target] || 0) + 1;
});
nodes.forEach(n => { n.degree = degreeMap[n.id] || 1; });
const maxDeg = Math.max(...nodes.map(n => n.degree));
const minDeg = Math.min(...nodes.map(n => n.degree));
const nodeRadius = d => {
const t = (d.degree - minDeg) / (maxDeg - minDeg || 1);
return 8 + t * 28; // 8px → 36px
};
let simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(120))
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(W / 2, H / 2))
.force("collision", d3.forceCollide().radius(d => nodeRadius(d) + 6));
// ═══════════════════════════════════════════════════════════════════════════
// DRAW LINKS
// ═══════════════════════════════════════════════════════════════════════════
const linkGroup = root.append("g").attr("class", "links");
const labelGroup = root.append("g").attr("class", "edge-labels");
const nodeGroup = root.append("g").attr("class", "nodes");
const link = linkGroup.selectAll("line")
.data(links)
.join("line")
.attr("class", "link")
.attr("marker-end", "url(#arrow)");
const edgeLabel = labelGroup.selectAll("text")
.data(links)
.join("text")
.attr("class", "edge-label")
.text(d => d.label || "");
// ═══════════════════════════════════════════════════════════════════════════
// DRAW NODES
// ═══════════════════════════════════════════════════════════════════════════
const node = nodeGroup.selectAll("g")
.data(nodes)
.join("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragStart)
.on("drag", dragged)
.on("end", dragEnd));
// Circle
node.append("circle")
.attr("r", nodeRadius)
.attr("fill", d => nodeColor(d.type));
// Label (shown below circle for large nodes, hidden for tiny ones)
node.append("text")
.attr("dy", d => nodeRadius(d) + 11)
.text(d => d.label.length > 20 ? d.label.slice(0, 18) + "…" : d.label)
.style("font-size", d => d.degree > 3 ? "11px" : "9px")
.style("opacity", d => d.degree > 2 ? 1 : 0.5);
// ═══════════════════════════════════════════════════════════════════════════
// SIMULATION TICK
// ═══════════════════════════════════════════════════════════════════════════
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);
edgeLabel
.attr("x", d => (d.source.x + d.target.x) / 2)
.attr("y", d => (d.source.y + d.target.y) / 2);
node.attr("transform", d => `translate(${d.x},${d.y})`);
});
// ═══════════════════════════════════════════════════════════════════════════
// TOOLTIP & HOVER
// ═══════════════════════════════════════════════════════════════════════════
let selectedNode = null;
node.on("mouseover", (e, d) => {
const connections = links.filter(l =>
l.source.id === d.id || l.target.id === d.id
).length;
tooltip.innerHTML = `
<div class="tt-name">${d.label}</div>
<div class="tt-type" style="color:${nodeColor(d.type)}">${d.type}</div>
${d.description ? `<div class="tt-desc">${d.description}</div>` : ""}
<div class="tt-connections"><span>${connections}</span> connection${connections !== 1 ? "s" : ""}</div>
`;
tooltip.classList.add("visible");
positionTooltip(e);
})
.on("mousemove", positionTooltip)
.on("mouseleave", () => tooltip.classList.remove("visible"))
.on("click", (e, d) => {
e.stopPropagation();
if (selectedNode === d.id) {
clearSelection();
} else {
selectNode(d);
}
// Plain click only selects/highlights — exploring the graph must not spawn
// a file-explorer window on every click. Shift+click or Ctrl+click opens
// the node's storage folder or link (local path or URL — see osutil.open_location).
if ((e.shiftKey || e.ctrlKey) && window.pyBridge && d.path) window.pyBridge.openPath(d.path);
});
svg.on("click", clearSelection);
function positionTooltip(e) {
const rect = container.getBoundingClientRect();
let x = e.clientX - rect.left + 14;
let y = e.clientY - rect.top - 10;
if (x + 280 > W) x = e.clientX - rect.left - 280;
tooltip.style.left = x + "px";
tooltip.style.top = y + "px";
}
function selectNode(d) {
selectedNode = d.id;
const connectedIds = new Set([d.id]);
links.forEach(l => {
if (l.source.id === d.id) connectedIds.add(l.target.id);
if (l.target.id === d.id) connectedIds.add(l.source.id);
});
node.classed("selected", n => n.id === d.id);
node.classed("dimmed", n => !connectedIds.has(n.id));
link.classed("highlighted", l => l.source.id === d.id || l.target.id === d.id);
link.classed("dimmed", l => l.source.id !== d.id && l.target.id !== d.id);
}
function clearSelection() {
selectedNode = null;
node.classed("selected dimmed", false);
link.classed("highlighted dimmed", false);
}
// ═══════════════════════════════════════════════════════════════════════════
// DRAG
// ═══════════════════════════════════════════════════════════════════════════
function dragStart(e, d) {
if (!e.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; d.fy = d.y;
}
function dragged(e, d) { d.fx = e.x; d.fy = e.y; }
function dragEnd(e, d) {
if (!e.active) simulation.alphaTarget(0);
d.fx = null; d.fy = null;
}
// ═══════════════════════════════════════════════════════════════════════════
// LEGEND
// ═══════════════════════════════════════════════════════════════════════════
const typeCounts = {};
nodes.forEach(n => { typeCounts[n.type] = (typeCounts[n.type] || 0) + 1; });
const hiddenTypes = new Set();
const legendEl = document.getElementById("legend");
Object.entries(COLOR_MAP).forEach(([type, color]) => {
if (!typeCounts[type]) return;
const item = document.createElement("div");
item.className = "legend-item";
item.innerHTML = `
<div class="legend-dot" style="background:${color}"></div>
<span class="legend-name">${type}</span>
<span class="legend-count">${typeCounts[type]}</span>
`;
item.addEventListener("click", () => {
if (hiddenTypes.has(type)) {
hiddenTypes.delete(type);
item.classList.remove("disabled");
} else {
hiddenTypes.add(type);
item.classList.add("disabled");
}
applyTypeFilter();
});
legendEl.appendChild(item);
});
function applyTypeFilter() {
node.style("display", d => hiddenTypes.has(d.type) ? "none" : null);
link.style("display", d => {
const sHidden = hiddenTypes.has(d.source.type);
const tHidden = hiddenTypes.has(d.target.type);
return sHidden || tHidden ? "none" : null;
});
}
// ═══════════════════════════════════════════════════════════════════════════
// SEARCH
// ═══════════════════════════════════════════════════════════════════════════
document.getElementById("search").addEventListener("input", e => {
const q = e.target.value.toLowerCase().trim();
if (!q) { clearSelection(); return; }
const match = nodes.find(n => n.label.toLowerCase().includes(q));
if (match) selectNode(match);
});
// ═══════════════════════════════════════════════════════════════════════════
// CONTROLS
// ═══════════════════════════════════════════════════════════════════════════
document.getElementById("link-distance").addEventListener("input", e => {
simulation.force("link").distance(+e.target.value);
simulation.alpha(0.3).restart();
});
document.getElementById("charge").addEventListener("input", e => {
simulation.force("charge").strength(+e.target.value);
simulation.alpha(0.3).restart();
});
// Show-relationship: toggles the relation-name labels ON the edges. When off,
// the edges themselves stay — only their relationship names are hidden.
function applyShowRel(){
const on = document.getElementById("show-rel").checked;
edgeLabel.classed("visible", on);
}
document.getElementById("show-rel").addEventListener("change", applyShowRel);
document.getElementById("btn-reset").addEventListener("click", () => {
// Reset zoom = auto-fit the whole graph into view (defined below as fitGraph).
if (window.fitGraph) window.fitGraph();
});
document.getElementById("btn-all").addEventListener("click", () => {
hiddenTypes.clear();
document.querySelectorAll(".legend-item").forEach(el => el.classList.remove("disabled"));
applyTypeFilter();
clearSelection();
});
// Show the relationship names on the edges by default (the in-graph
// "Show relationship" checkbox toggles them; edges always stay visible).
applyShowRel();
// ═══════════════════════════════════════════════════════════════════════════
// STATS
// ═══════════════════════════════════════════════════════════════════════════
document.getElementById("stat-nodes").textContent = nodes.length;
document.getElementById("stat-edges").textContent = links.length;
document.getElementById("stat-communities").textContent =
GRAPH_DATA.communities !== undefined ? GRAPH_DATA.communities : "—";
// ═══════════════════════════════════════════════════════════════════════════
// RESIZE
// ═══════════════════════════════════════════════════════════════════════════
window.addEventListener("resize", () => {
W = container.clientWidth;
H = container.clientHeight;
simulation.force("center", d3.forceCenter(W / 2, H / 2));
simulation.alpha(0.1).restart();
});
// ═══════════════════════════════════════════════════════════════════════════
// FIT — frame all nodes in view (called from the app's Fit button)
// ═══════════════════════════════════════════════════════════════════════════
window.fitGraph = function () {
if (!nodes.length) return;
const xs = nodes.map(n => n.x), ys = nodes.map(n => n.y);
const minX = Math.min(...xs), maxX = Math.max(...xs);
const minY = Math.min(...ys), maxY = Math.max(...ys);
const gw = (maxX - minX) || 1, gh = (maxY - minY) || 1;
const scale = Math.max(0.1, Math.min(W / (gw + 140), H / (gh + 140), 2));
const tx = W / 2 - scale * (minX + maxX) / 2;
const ty = H / 2 - scale * (minY + maxY) / 2;
svg.transition().duration(400).call(zoom.transform,
d3.zoomIdentity.translate(tx, ty).scale(scale));
};
// ═══════════════════════════════════════════════════════════════════════════
// EXPORT PNG — called from the app's Export button (structure_graph_view.py).
// Renders the CURRENT on-screen view (same pan/zoom/colors) to a PNG data URL:
// grabbing this widget's pixels directly (QWidget.grab()) is unreliable for a
// QWebEngineView (its content is composited by Chromium, not Qt's own paint
// system, so a plain grab() often comes back blank) — rasterizing the SVG
// ourselves via an offscreen <canvas> sidesteps that entirely.
// ═══════════════════════════════════════════════════════════════════════════
window.exportPng = function () {
return new Promise(resolve => {
try {
const svgEl = document.getElementById("graph");
const rect = svgEl.getBoundingClientRect();
const width = Math.max(1, Math.round(rect.width)) || 1600;
const height = Math.max(1, Math.round(rect.height)) || 1000;
const clone = svgEl.cloneNode(true);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.setAttribute("width", width);
clone.setAttribute("height", height);
// An isolated SVG document has no access to this page's <style> rules —
// inline every rule (including the :root custom-property definitions
// colors/strokes are built from) so the export matches what's on screen.
let cssText = "";
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules || []) cssText += rule.cssText + "\n";
} catch (e) { /* cross-origin stylesheet — nothing of ours is, but be safe */ }
}
const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
styleEl.textContent = cssText;
clone.insertBefore(styleEl, clone.firstChild);
// Solid background rect — the SVG itself has none (CSS paints the
// container behind it), so without this the PNG would be transparent.
const bg = getComputedStyle(document.documentElement).getPropertyValue("--bg").trim() || "#14151a";
const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bgRect.setAttribute("width", String(width));
bgRect.setAttribute("height", String(height));
bgRect.setAttribute("fill", bg);
clone.insertBefore(bgRect, clone.firstChild.nextSibling);
const xml = new XMLSerializer().serializeToString(clone);
const svgUrl = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(xml)));
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.getContext("2d").drawImage(img, 0, 0, width, height);
resolve(canvas.toDataURL("image/png"));
};
img.onerror = () => resolve("");
img.src = svgUrl;
} catch (e) {
resolve("");
}
});
};
// ═══════════════════════════════════════════════════════════════════════════
// PYTHON BRIDGE (open the storage folder when a node is clicked)
// ═══════════════════════════════════════════════════════════════════════════
if (window.qt && qt.webChannelTransport) {
new QWebChannel(qt.webChannelTransport, ch => { window.pyBridge = ch.objects.py; });
} else if (location.protocol === "http:") {
// Served by the app's local GraphServer (browser mode — builds without
// QtWebEngine, e.g. the standalone .exe): relay node clicks back over HTTP.
const _token = new URLSearchParams(location.search).get("t") || "";
window.pyBridge = {
openPath: p => fetch(`/open?t=${encodeURIComponent(_token)}&path=${encodeURIComponent(p)}`)
.catch(() => {}),
};
}
</script>
</body>
</html>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB