Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 70 additions & 26 deletions threat_analysis/server/templates/simple_mode.html
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,7 @@ <h2>Generate Threat Model with AI</h2>

<script>
let panZoomInstance;
let _panZoomRafId = null;
let debounceTimer;
let _validateTimer = null;
let _diagramInFlight = false;
Expand Down Expand Up @@ -1245,16 +1246,31 @@ <h2>Generate Threat Model with AI</h2>
updatePreview();
},

openOrSwitchToTab: function(path) {
const existingIndex = this.editors.findIndex(e => e && e.path === path);
if (existingIndex !== -1) {
this.switchTab(existingIndex);
openOrSwitchToTab: function(submodelRef) {
// Extract the filename (basename) from the ref, ignoring directory parts.
// Works for any form: "./foo/bar.md", "../foo/bar.md", "bar.md"
const basename = submodelRef.split('/').pop().split('\\').pop();

// Search all open tabs by filename — path-based matching is unreliable
// because tabs use project-root-relative paths while DSL refs are
// relative to the current model file.
const idx = this.editors.findIndex(
e => e && e.path.split('/').pop().split('\\').pop() === basename
);
if (idx !== -1) { this.switchTab(idx); return; }

// No match found — when a full project was loaded via picker the
// referenced sub-model should already be in the tabs; opening a blank
// tab would cause an empty SVG and an svg-pan-zoom crash.
if (this.projectLoaded) {
console.warn(`openOrSwitchToTab: no open tab matches "${basename}" — load the full project directory first.`);
return;
}
// Tab doesn't exist yet — create it so the user can edit the sub-model
const filename = path.split('/').pop().split('\\').pop();
const modelName = filename.replace(/\.md$/, '') || 'Sub-Model';
const newIndex = this.addTab(path, `# Threat Model: ${modelName}\n\n`);

// Fallback for server-managed mode: create a new tab so the user
// can start editing the referenced sub-model.
const stem = basename.replace(/\.md$/, '') || 'Sub-Model';
const newIndex = this.addTab(basename, `# Threat Model: ${stem}\n\n`);
this.switchTab(newIndex);
},

Expand All @@ -1279,8 +1295,10 @@ <h2>Generate Threat Model with AI</h2>
// Explicitly clear the preview as no valid model is active
svgWrapper.innerHTML = '';
legendContainer.innerHTML = '';
if (panZoomInstance) panZoomInstance.destroy();
panZoomInstance = null;
if (panZoomInstance) {
try { panZoomInstance.destroy(); } catch(e) { /* already destroyed */ }
panZoomInstance = null;
}
globalGraphMetadata = null;
}
},
Expand All @@ -1307,7 +1325,7 @@ <h2>Generate Threat Model with AI</h2>
document.getElementById('svg-wrapper').innerHTML = '';
document.getElementById('legend-container').innerHTML = '';
if (panZoomInstance) {
panZoomInstance.destroy();
try { panZoomInstance.destroy(); } catch(e) { /* already destroyed */ }
panZoomInstance = null;
}
globalGraphMetadata = null;
Expand Down Expand Up @@ -1496,14 +1514,20 @@ <h2>Generate Threat Model with AI</h2>
showErrorMessage(`Error updating diagram: ${errorData.error}`);
svgWrapper.innerHTML = '';
legendContainer.innerHTML = '';
if (panZoomInstance) panZoomInstance.destroy();
panZoomInstance = null;
if (panZoomInstance) {
try { panZoomInstance.destroy(); } catch(e) { /* already destroyed */ }
panZoomInstance = null;
}
globalGraphMetadata = null; // Clear metadata on error
return;
}
const data = await response.json();

if (panZoomInstance) panZoomInstance.destroy();
if (_panZoomRafId !== null) { cancelAnimationFrame(_panZoomRafId); _panZoomRafId = null; }
if (panZoomInstance) {
try { panZoomInstance.destroy(); } catch(e) { /* already destroyed */ }
panZoomInstance = null;
}
svgWrapper.innerHTML = data.diagram_svg;
legendContainer.innerHTML = data.legend_html;
globalGraphMetadata = data.graph_metadata; // Store graph metadata
Expand All @@ -1515,17 +1539,35 @@ <h2>Generate Threat Model with AI</h2>

const svgElement = svgWrapper.querySelector('svg');
if (svgElement) {
console.log("SVG element found in svgWrapper, initializing svgPanZoom on SVG element.");
panZoomInstance = svgPanZoom(svgElement, {
zoomEnabled: true,
panEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.1,
maxZoom: 10
// Cancel any pending RAF from a previous updatePreview call so only
// the most-recent SVG gets svg-pan-zoom initialized. Without this,
// rapid tab switches schedule multiple RAFs; the earlier ones fire on
// SVG elements already detached from the DOM, causing a non-finite
// SVGMatrix and an uncaught error inside svg-pan-zoom's event handler.
if (_panZoomRafId !== null) {
cancelAnimationFrame(_panZoomRafId);
_panZoomRafId = null;
}
_panZoomRafId = requestAnimationFrame(() => {
_panZoomRafId = null;
// Guard: skip if this SVG was replaced before the RAF fired.
if (!svgElement.isConnected) return;
try {
panZoomInstance = svgPanZoom(svgElement, {
zoomEnabled: true,
panEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.1,
maxZoom: 10
});
} catch(e) {
console.warn('svgPanZoom init failed:', e);
panZoomInstance = null;
}
});
console.log("svgPanZoom initialized with instance:", panZoomInstance);
console.log("svgPanZoom scheduled for next frame.");

// Add a single click listener to the SVG container for event delegation
svgElement.addEventListener('click', (e) => {
Expand Down Expand Up @@ -1571,8 +1613,10 @@ <h2>Generate Threat Model with AI</h2>
showErrorMessage(`Failed to fetch preview: ${error.message}`);
svgWrapper.innerHTML = '';
legendContainer.innerHTML = '';
if (panZoomInstance) panZoomInstance.destroy();
panZoomInstance = null;
if (panZoomInstance) {
try { panZoomInstance.destroy(); } catch(e) { /* already destroyed */ }
panZoomInstance = null;
}
globalGraphMetadata = null; // Clear metadata on error
} finally {
_diagramInFlight = false;
Expand Down
Loading