prvng_platform/crates/provisioning-daemon/ui/templates/pages/ontology.html

249 lines
7 KiB
HTML
Raw Permalink Normal View History

{% extends "base.html" %}
{% block title %}Ontology{% endblock %}
{% block nav_ontology %}btn-active{% endblock %}
{% block main_class %}p-0{% endblock %}
{% block head %}
<style>
#graph-root {
display: flex;
height: calc(100vh - 64px - 40px);
min-height: 500px;
}
#cy-wrapper {
flex: 1 1 auto;
min-width: 0;
position: relative;
}
#cy { width: 100%; height: 100%; }
#cy-controls {
position: absolute;
bottom: 14px;
right: 14px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 4px;
}
#cy-controls button {
width: 30px; height: 30px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(0,0,0,0.6);
color: #fff;
font-size: 16px;
cursor: pointer;
}
#cy-controls button:hover { background: rgba(80,80,80,0.8); }
#sidebar {
width: 280px;
flex-shrink: 0;
overflow-y: auto;
background: oklch(var(--b2, 20% 0 0));
border-left: 1px solid oklch(var(--b3, 15% 0 0));
padding: 1rem;
font-size: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="px-4 pt-4 pb-2 flex items-center gap-4">
<h1 class="text-lg font-bold font-mono">Domain Ontology</h1>
<div class="flex gap-2 text-xs">
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" id="show-axioms" checked class="checkbox checkbox-xs">
<span class="badge badge-primary badge-xs">Axioms</span>
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" id="show-practices" checked class="checkbox checkbox-xs">
<span class="badge badge-success badge-xs">Practices</span>
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input type="checkbox" id="show-tensions" checked class="checkbox checkbox-xs">
<span class="badge badge-warning badge-xs">Tensions</span>
</label>
</div>
{% if not has_root %}
<div class="alert alert-warning alert-xs py-1 px-3 text-xs">
--project-root not set — graph will be empty
</div>
{% endif %}
</div>
<div id="graph-root">
<div id="cy-wrapper">
<div id="cy"></div>
<div id="cy-controls">
<button onclick="cy.fit()" title="Fit"></button>
<button onclick="cy.zoom(cy.zoom()*1.25)" title="Zoom in">+</button>
<button onclick="cy.zoom(cy.zoom()*0.8)" title="Zoom out"></button>
<button onclick="resetLayout()" title="Re-layout"></button>
</div>
</div>
<div id="sidebar">
<p class="text-base-content/40 text-xs mb-3">Click a node to inspect.</p>
<div id="node-detail"></div>
</div>
</div>
<script>
const GRAPH = {{ graph_json | safe }};
let cy = null;
function levelColor(level) {
if (level === 'Axiom') return '#6366f1';
if (level === 'Practice') return '#22c55e';
if (level === 'Tension') return '#f59e0b';
return '#6b7280';
}
function buildElements() {
const nodes = (GRAPH.nodes || []).map(n => ({
data: {
id: n.id,
label: n.name || n.id,
level: n.level,
description: n.description || '',
color: levelColor(n.level),
invariant: n.invariant || false,
artifact_paths: n.artifact_paths || [],
}
}));
const edges = (GRAPH.edges || []).map((e, i) => ({
data: {
id: 'e' + i,
source: e.from,
target: e.to,
kind: e.kind,
note: e.note || '',
weight: e.weight,
}
}));
return [...nodes, ...edges];
}
function initCytoscape() {
cy = cytoscape({
container: document.getElementById('cy'),
elements: buildElements(),
style: [
{
selector: 'node',
style: {
'background-color': 'data(color)',
'label': 'data(label)',
'color': '#f1f5f9',
'font-size': '8px',
'font-family': 'ui-monospace, monospace',
'text-valign': 'center',
'text-halign': 'center',
'text-wrap': 'wrap',
'text-max-width': '56px',
'text-overflow-wrap': 'anywhere',
'width': 72,
'height': 72,
'border-width': 2,
'border-color': 'rgba(255,255,255,0.15)',
}
},
{
selector: 'node[?invariant]',
style: {
'border-width': 3,
'border-color': '#a5b4fc',
'border-style': 'double',
}
},
{
selector: 'edge',
style: {
'width': 1.5,
'line-color': 'rgba(156,163,175,0.4)',
'target-arrow-color': 'rgba(156,163,175,0.6)',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier',
'arrow-scale': 0.8,
}
},
{
selector: 'node:selected',
style: {
'border-color': '#818cf8',
'border-width': 3,
}
},
],
layout: {
name: 'cose',
animate: false,
nodeRepulsion: 12000,
idealEdgeLength: 140,
gravity: 0.15,
numIter: 1000,
fit: true,
padding: 30,
},
});
cy.on('tap', 'node', function(evt) {
const d = evt.target.data();
const paths = d.artifact_paths.length
? d.artifact_paths.map(p => `<code class="font-mono">${p}</code>`).join('<br>')
: '<span class="text-base-content/30"></span>';
document.getElementById('node-detail').innerHTML = `
<div class="mb-2">
<span class="badge badge-xs" style="background:${d.color}">${d.level}</span>
<span class="font-mono font-semibold ml-1">${d.id}</span>
</div>
<p class="font-semibold text-sm mb-1">${d.label}</p>
<p class="text-base-content/60 mb-3">${d.description}</p>
<div class="text-base-content/40 text-xs">${paths}</div>
`;
});
cy.on('tap', function(evt) {
if (evt.target === cy) document.getElementById('node-detail').innerHTML = '';
});
} // end initCytoscape
function resetLayout() {
if (cy) cy.layout({ name: 'cose', animate: true, nodeRepulsion: 12000, idealEdgeLength: 140, gravity: 0.15, numIter: 1000 }).run();
}
// Filter checkboxes
function applyFilters() {
if (!cy) return;
const showAxioms = document.getElementById('show-axioms').checked;
const showPractices = document.getElementById('show-practices').checked;
const showTensions = document.getElementById('show-tensions').checked;
cy.nodes().forEach(n => {
const l = n.data('level');
const vis = (l === 'Axiom' && showAxioms)
|| (l === 'Practice' && showPractices)
|| (l === 'Tension' && showTensions)
|| (!['Axiom','Practice','Tension'].includes(l));
n.style('display', vis ? 'element' : 'none');
});
cy.edges().forEach(e => {
const srcVis = e.source().style('display') !== 'none';
const tgtVis = e.target().style('display') !== 'none';
e.style('display', srcVis && tgtVis ? 'element' : 'none');
});
}
['show-axioms','show-practices','show-tensions'].forEach(id => {
document.getElementById(id).addEventListener('change', applyFilters);
});
if (typeof cytoscape !== 'undefined') {
initCytoscape();
} else {
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/cytoscape@3.30.2/dist/cytoscape.min.js';
s.onload = initCytoscape;
document.head.appendChild(s);
}
</script>
{% endblock %}