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

248 lines
7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 %}