bolt-new / index.html
00Boobs00's picture
Upload folder using huggingface_hub
930cf1b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeuroArch Studio | AI Architecture & Prompt Engineering Suite</title>
<!-- Importing Inter and JetBrains Mono for a clean, technical aesthetic -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
/* Color Palette - Deep Space & Neon Cyber */
--bg-deep: #050507;
--bg-panel: #0f1115;
--bg-surface: #181b21;
--primary: #3b82f6; /* Electric Blue */
--primary-glow: rgba(59, 130, 246, 0.5);
--accent: #8b5cf6; /* Violet */
--accent-glow: rgba(139, 92, 246, 0.5);
--success: #10b981;
--warning: #f59e0b;
--text-main: #e2e8f0;
--text-muted: #94a3b8;
--border: #2d3748;
--grid-line: rgba(255, 255, 255, 0.03);
/* Spacing & Radius */
--radius-md: 8px;
--radius-lg: 16px;
--header-height: 64px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-deep);
color: var(--text-main);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* --- Header --- */
header {
height: var(--header-height);
background: rgba(15, 17, 21, 0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
z-index: 100;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 1.25rem;
letter-spacing: -0.025em;
background: linear-gradient(to right, var(--primary), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.brand-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), var(--accent));
border-radius: var(--radius-md);
display: grid;
place-items: center;
color: white;
font-size: 1.2rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 20px;
}
a.built-with {
color: var(--text-muted);
text-decoration: none;
font-size: 0.85rem;
transition: color 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
a.built-with:hover {
color: var(--primary);
}
/* --- Main Layout --- */
main {
flex: 1;
display: grid;
grid-template-columns: 280px 1fr 320px;
height: calc(100vh - var(--header-height));
position: relative;
}
/* --- Sidebar (Tools) --- */
.sidebar {
background: var(--bg-panel);
border-right: 1px solid var(--border);
padding: 20px;
display: flex;
flex-direction: column;
gap: 24px;
overflow-y: auto;
}
.tool-group h3 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 12px;
}
.node-btn {
width: 100%;
background: var(--bg-surface);
border: 1px solid var(--border);
color: var(--text-main);
padding: 12px;
border-radius: var(--radius-md);
cursor: pointer;
text-align: left;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
font-size: 0.9rem;
}
.node-btn:hover {
border-color: var(--primary);
background: rgba(59, 130, 246, 0.05);
transform: translateX(4px);
}
.node-btn .icon {
width: 24px;
height: 24px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
/* --- Canvas Area --- */
.canvas-container {
background-color: var(--bg-deep);
background-image:
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 40px 40px;
position: relative;
overflow: hidden;
user-select: none;
cursor: grab;
}
.canvas-container:active {
cursor: grabbing;
}
/* SVG Overlay for Connections */
#connections-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.connection-path {
fill: none;
stroke: var(--border);
stroke-width: 2px;
transition: stroke 0.3s;
}
.connection-path.active {
stroke: var(--primary);
stroke-dasharray: 10;
animation: flowAnimation 1s linear infinite;
}
@keyframes flowAnimation {
from { stroke-dashoffset: 20; }
to { stroke-dashoffset: 0; }
}
/* Nodes on Canvas */
.node {
position: absolute;
width: 180px;
background: rgba(24, 27, 33, 0.9);
backdrop-filter: blur(8px);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
z-index: 2;
cursor: pointer;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s, border-color 0.2s;
}
.node:hover {
border-color: var(--text-muted);
}
.node.selected {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2), 0 10px 15px -3px rgba(0, 0, 0, 0.5);
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-weight: 600;
font-size: 0.9rem;
}
.node-status {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--text-muted);
}
.node-status.active { background-color: var(--success); box-shadow: 0 0 8px var(--success); }
.node-status.processing { background-color: var(--warning); box-shadow: 0 0 8px var(--warning); }
.node-body {
font-size: 0.8rem;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
.node-ports {
position: absolute;
width: 12px;
height: 12px;
background: var(--text-main);
border-radius: 50%;
border: 2px solid var(--bg-surface);
cursor: crosshair;
}
.port-in { top: -6px; left: 50%; transform: translateX(-50%); }
.port-out { bottom: -6px; left: 50%; transform: translateX(-50%); }
/* --- Right Panel (Properties) --- */
.properties-panel {
background: var(--bg-panel);
border-left: 1px solid var(--border);
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.panel-header {
font-size: 1rem;
font-weight: 600;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.prop-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.prop-group label {
font-size: 0.8rem;
color: var(--text-muted);
}
.prop-input, .prop-select, .prop-textarea {
background: var(--bg-surface);
border: 1px solid var(--border);
color: var(--text-main);
padding: 10px;
border-radius: var(--radius-md);
font-family: inherit;
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s;
}
.prop-input:focus, .prop-select:focus, .prop-textarea:focus {
border-color: var(--primary);
}
.prop-textarea {
resize: vertical;
min-height: 80px;
font-family: 'JetBrains Mono', monospace;
}
.action-btn {
background: var(--primary);
color: white;
border: none;
padding: 12px;
border-radius: var(--radius-md);
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
margin-top: auto;
}
.action-btn:hover {
background: #2563eb;
}
.action-btn.secondary {
background: transparent;
border: 1px solid var(--border);
color: var(--text-main);
}
.action-btn.secondary:hover {
border-color: var(--text-muted);
}
/* --- Console/Logs --- */
.console-output {
background: #000;
border-radius: var(--radius-md);
padding: 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
height: 150px;
overflow-y: auto;
color: #33ff00;
border: 1px solid #333;
margin-top: 20px;
}
.log-entry { margin-bottom: 4px; }
.log-time { color: #666; margin-right: 8px; }
.log-info { color: #3b82f6; }
.log-warn { color: #f59e0b; }
.log-success { color: #10b981; }
/* --- Toast Notification --- */
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: var(--bg-surface);
border: 1px solid var(--border);
border-left: 4px solid var(--primary);
padding: 16px;
border-radius: var(--radius-md);
min-width: 300px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease-out;
display: flex;
align-items: center;
justify-content: space-between;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* --- Responsive --- */
@media (max-width: 1024px) {
main {
grid-template-columns: 240px 1fr;
}
.properties-panel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 300px;
transform: translateX(100%);
transition: transform 0.3s;
z-index: 50;
background: var(--bg-panel);
}
.properties-panel.open {
transform: translateX(0);
}
}
@media (max-width: 768px) {
main {
grid-template-columns: 1fr;
}
.sidebar {
display: none; /* Hide sidebar on mobile for simplicity in this demo, would use hamburger in full app */
}
/* Add a floating action button for mobile to add nodes would be ideal here */
}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="brand-icon">N</div>
<span>NeuroArch Studio</span>
</div>
<div class="header-actions">
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">
<span>Built with anycoder</span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
</div>
</header>
<main>
<!-- Sidebar: Component Library -->
<aside class="sidebar">
<div class="tool-group">
<h3>Data Ingestion</h3>
<button class="node-btn" onclick="app.addNode('input')">
<div class="icon">📥</div> Data Source
</button>
<button class="node-btn" onclick="app.addNode('vector-db')">
<div class="icon">🗃️</div> Vector Store
</button>
</div>
<div class="tool-group">
<h3>AI Processing</h3>
<button class="node-btn" onclick="app.addNode('llm')">
<div class="icon">🧠</div> LLM Core
</button>
<button class="node-btn" onclick="app.addNode('embedder')">
<div class="icon">🔢</div> Embeddings
</button>
</div>
<div class="tool-group">
<h3>Output & Logic</h3>
<button class="node-btn" onclick="app.addNode('router')">
<div class="icon">🔀</div> Semantic Router
</button>
<button class="node-btn" onclick="app.addNode('output')">
<div class="icon">📤</div> Response
</button>
</div>
<div style="margin-top: auto;">
<button class="action-btn secondary" onclick="app.clearCanvas()" style="width: 100%; font-size: 0.8rem; padding: 8px;">Clear Canvas</button>
</div>
</aside>
<!-- Main Workspace -->
<section class="canvas-container" id="canvas">
<svg id="connections-layer"></svg>
<!-- Nodes will be injected here via JS -->
</section>
<!-- Right Panel: Properties & Config -->
<aside class="properties-panel" id="properties-panel">
<div class="panel-header">Properties</div>
<div id="no-selection-msg" style="color: var(--text-muted); font-size: 0.9rem; text-align: center; margin-top: 40px;">
Select a node to configure its parameters.
</div>
<div id="prop-form" style="display: none; flex-direction: column; height: 100%;">
<div class="prop-group">
<label>Node ID</label>
<input type="text" id="prop-id" class="prop-input" disabled>
</div>
<div class="prop-group">
<label>Label</label>
<input type="text" id="prop-label" class="prop-input">
</div>
<div class="prop-group" id="group-model">
<label>Model</label>
<select id="prop-model" class="prop-select">
<option value="gpt-4-turbo">GPT-4 Turbo</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
<option value="claude-3-opus">Claude 3 Opus</option>
<option value="mistral-large">Mistral Large</option>
<option value="llama-3-70b">Llama 3 70B</option>
</select>
</div>
<div class="prop-group" id="group-temp">
<label>Temperature (0.0 - 1.0)</label>
<input type="range" id="prop-temp" min="0" max="1" step="0.1" value="0.7" style="width: 100%">
<div style="display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-muted);">
<span>Precise</span>
<span id="temp-val">0.7</span>
<span>Creative</span>
</div>
</div>
<div class="prop-group">
<label>System Prompt / Context</label>
<textarea id="prop-prompt" class="prop-textarea" placeholder="Enter system instructions..."></textarea>
</div>
<button class="action-btn" onclick="app.runSimulation()">Run Simulation</button>
<button class="action-btn secondary" onclick="app.deleteSelectedNode()">Delete Node</button>
</div>
<div class="console-output" id="console-logs">
<div class="log-entry"><span class="log-time">[System]</span> Ready. Awaiting architecture input.</div>
</div>
</aside>
</main>
<div class="toast-container" id="toast-container"></div>
<script>
/**
* NeuroArch Studio Core Application Logic
* Implements a node-based architecture for AI system design.
* Designed with modularity and scalability in mind.
*/
class NeuroArchApp {
constructor() {
this.nodes = [];
this.connections = [];
this.selectedNodeId = null;
this.canvas = document.getElementById('canvas');
this.svgLayer = document.getElementById('connections-layer');
this.isDragging = false;
this.dragNodeId = null;
this.dragOffset = { x: 0, y: 0 };
// UI References
this.propPanel = document.getElementById('prop-form');
this.noSelectionMsg = document.getElementById('no-selection-msg');
this.consoleLogs = document.getElementById('console-logs');
// Bind events
this.canvas.addEventListener('mousedown', (e) => this.handleCanvasMouseDown(e));
document.addEventListener('mousemove', (e) => this.handleMouseMove(e));
document.addEventListener('mouseup', (e) => this.handleMouseUp(e));
// Initialize with a default node
this.addNode('input', 100, 150);
}
/**
* Generates a unique ID for nodes
*/
generateId() {
return 'node_' + Math.random().toString(36).substr(2, 9);
}
/**
* Logs messages to the system console
*/
log(message, type = 'info') {
const time = new Date().toLocaleTimeString('en-US', { hour12: false });
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `<span class="log-time">[${time}]</span> <span class="log-${type}">${message}</span>`;
this.consoleLogs.appendChild(entry);
this.consoleLogs.scrollTop = this.consoleLogs.scrollHeight;
}
/**
* Creates a new node and adds it to the canvas
*/
addNode(type, x = 50, y = 50) {
const id = this.generateId();
const nodeData = {
id,
type,
x,
y,
label: this.getDefaultLabel(type),
model: type === 'llm' ? 'gpt-4-turbo' : 'N/A',
temp: 0.7,
prompt: ''
};
this.nodes.push(nodeData);
this.renderNode(nodeData);
this.log(`Added node: ${nodeData.label} (${type})`, 'info');
return id;
}
getDefaultLabel(type) {
const labels = {
'input': 'Data Source',
'vector-db': 'Vector DB',
'llm': 'LLM Processor',
'embedder': 'Text Embedder',
'router': 'Semantic Router',
'output': 'Final Output'
};
return labels[type] || 'Unknown Node';
}
/**
* Renders the DOM element for a node
*/
renderNode(node) {
const el = document.createElement('div');
el.className = 'node';
el.id = node.id;
el.style.left = `${node.x}px`;
el.style.top = `${node.y}px`;
// Inner HTML structure
el.innerHTML = `
<div class="node-ports port-in" data-port="in" title="Input"></div>
<div class="node-header">
<span>${node.label}</span>
<div class="node-status" id="status-${node.id}"></div>
</div>
<div class="node-body">
${node.type.toUpperCase()}<br>
Latency: <span id="latency-${node.id}">--</span>ms
</div>
<div class="node-ports port-out" data-port="out" title="Output"></div>
`;
// Selection Event
el.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('node-ports')) return; // Let port logic handle it
this.selectNode(node.id);
this.isDragging = true;
this.dragNodeId = node.id;
const rect = el.getBoundingClientRect();
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = e.clientY - rect.top;
});
this.canvas.appendChild(el);
// Simple animation entry
el.animate([
{ transform: 'scale(0.8)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
], { duration: 200, easing: 'ease-out' });
}
/**
* Handles mouse down on the canvas (deselect or start drag)
*/
handleCanvasMouseDown(e) {
if (e.target === this.canvas || e.target === this.svgLayer) {
this.deselectAll();
}
}
handleMouseMove(e) {
if (this.isDragging && this.dragNodeId) {
const node = this.nodes.find(n => n.id === this.dragNodeId);
if (node) {
// Calculate new position relative to canvas
const canvasRect = this.canvas.getBoundingClientRect();
let newX = e.clientX - canvasRect.left - this.dragOffset.x;
let newY = e.clientY - canvasRect.top - this.dragOffset.y;
// Boundary checks
newX = Math.max(0, newX);
newY = Math.max(0, newY);
node.x = newX;
node.y = newY;
// Update DOM
const el = document.getElementById(node.id);
el.style.left = `${newX}px`;
el.style.top = `${newY}px`;
this.updateConnections();
}
}
}
handleMouseUp(e) {
this.isDragging = false;
this.dragNodeId = null;
}
/**
* Selects a node and populates the property panel
*/
selectNode(id) {
this.selectedNodeId = id;
// Visual feedback
document.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
const el = document.getElementById(id);
if(el) el.classList.add('selected');
// Show properties
const node = this.nodes.find(n => n.id === id);
if (node) {
this.noSelectionMsg.style.display = 'none';
this.propPanel.style.display = 'flex';
// Populate fields
document.getElementById('prop-id').value = node.id;
document.getElementById('prop-label').value = node.label;
document.getElementById('prop-temp').value = node.temp;
document.getElementById('temp-val').innerText = node.temp;
document.getElementById('prop-prompt').value = node.prompt;
const modelSelect = document.getElementById('prop-model');
if(node.type === 'llm') {
document.getElementById('group-model').style.display = 'flex';
document.getElementById('group-temp').style.display = 'flex';
modelSelect.value = node.model;
} else {
document.getElementById('group-model').style.display = 'none';
document.getElementById('group-temp').style.display = 'none';
}
// Add event listeners for input changes to update state immediately
this.setupPropertyListeners(node);
}
}
setupPropertyListeners(node) {
const labelInput = document.getElementById('prop-label');
const tempInput = document.getElementById('prop-temp');
const promptInput = document.getElementById('prop-prompt');
const modelSelect = document.getElementById('prop-model');
// Remove old listeners to prevent stacking (simplified approach)
const newLabelInput = labelInput.cloneNode(true);
labelInput.parentNode.replaceChild(newLabelInput, labelInput);
newLabelInput.addEventListener('input', (e) => {
node.label = e.target.value;
document.querySelector(`#${node.id} .node-header span`).innerText = node.label;
});
const newTempInput = tempInput.cloneNode(true);
tempInput.parentNode.replaceChild(newTempInput, tempInput);
newTempInput.addEventListener('input', (e) => {
node.temp = e.target.value;
document.getElementById('temp-val').innerText = node.temp;
});
const newPromptInput = promptInput.cloneNode(true);
promptInput.parentNode.replaceChild(newPromptInput, promptInput);
newPromptInput.addEventListener('input', (e) => {
node.prompt = e.target.value;
});
const newModelSelect = modelSelect.cloneNode(true);
modelSelect.parentNode.replaceChild(newModelSelect, modelSelect);
newModelSelect.addEventListener('change', (e) => {
node.model = e.target.value;
this.log(`Updated model for ${node.id} to ${node.model}`, 'info');
});
}
deselectAll() {
this.selectedNodeId = null;
document.querySelectorAll('.node').forEach(n => n.classList.remove('selected'));
this.noSelectionMsg.style.display = 'block';
this.propPanel.style.display = 'none';
}
/**
* Logic to connect nodes (simplified auto-connection for demo)
*/
connectNodes(sourceId, targetId) {
// Check if already connected
const exists = this.connections.find(c => c.from === sourceId && c.to === targetId);
if (!exists) {
this.connections.push({ from: sourceId, to: targetId });
this.updateConnections();
}
}
/**
* Draws SVG lines between connected nodes
*/
updateConnections() {
this.svgLayer.innerHTML = ''; // Clear existing lines
this.connections.forEach(conn => {
const fromNode = document.getElementById(conn.from);
const toNode = document.getElementById(conn.to);
if (fromNode && toNode) {
const fromRect = fromNode.getBoundingClientRect();
const toRect = toNode.getBoundingClientRect();
const canvasRect = this.canvas.getBoundingClientRect();
// Calculate port positions (centers of bottom/top borders)
const x1 = fromRect.left - canvasRect.left + fromRect.width / 2;
const y1 = fromRect.top - canvasRect.top + fromRect.height;
const x2 = toRect.left - canvasRect.left + toRect.width / 2;
const y2 = toRect.top - canvasRect.top;
// Create Bezier Curve
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const controlY1 = y1 + 50;
const controlY2 = y2 - 50;
const d = `M ${x1} ${y1} C ${x1} ${controlY1}, ${x2} ${controlY2}, ${x2} ${y2}`;
path.setAttribute('d', d);
path.setAttribute('class', 'connection-path');
path.setAttribute('id', `conn-${conn.from}-${conn.to}`);
this.svgLayer.appendChild(path);
}
});
}
deleteSelectedNode() {
if(!this.selectedNodeId) return;
// Remove DOM
const el = document.getElementById(this.selectedNodeId);
if(el) el.remove();
// Remove from data
this.nodes = this.nodes.filter(n => n.id !== this.selectedNodeId);
this.connections = this.connections.filter(c => c.from !== this.selectedNodeId && c.to !== this.selectedNodeId);
this.log(`Deleted node ${this.selectedNodeId}`, 'warn');
this.deselectAll();
this.updateConnections();
}
clearCanvas() {
this.nodes.forEach(n => {
const el = document.getElementById(n.id);
if(el) el.remove();
});
this.nodes = [];
this.connections = [];
this.svgLayer.innerHTML = '';
this.log('Canvas cleared.', 'info');
this.deselectAll();
}
/**
* Simulates a data pipeline run through the connected nodes
*/
async runSimulation() {
if (this.connections.length === 0) {
this.showToast('No connections detected. Connect nodes to run pipeline.', 'warning');
return;
}
this.log('Initializing pipeline simulation...', 'info');
// Reset visuals
document.querySelectorAll('.node-status').forEach(el => {
el.className = 'node-status';
});
document.querySelectorAll('.connection-path').forEach(el => el.classList.remove('active'));
// Simplified Topological Sort / Flow Simulation
// For this demo, we just animate sequentially or breadth-first based on connections
const visited = new Set();
const queue = this.nodes.filter(n => this.connections.some(c => c.from === n.id)); // Start from sources
// Actually, let's just animate everything for effect in this UI demo
// In a real app, we'd traverse the graph.
for (const node of this.nodes) {
const statusEl = document.getElementById(`status-${node.id}`);
statusEl.classList.add('processing');
// Simulate processing time
const latency = Math.floor(Math.random() * 200) + 50;
document.getElementById(`latency-${node.id}`).innerText = latency;
await new Promise(r => setTimeout(r, latency));
statusEl.classList.remove('processing');
statusEl.classList.add('active');
// Animate outgoing connections
this.connections
.filter(c => c.from === node.id)
.forEach(c => {
const path = document.getElementById(`conn-${c.from}-${c.to}`);
if(path) path.classList.add('active');
});
this.log(`Executed ${node.label}: ${latency}ms`, 'success');
}
this.showToast('Pipeline execution completed successfully.', 'success');
}
showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast';
toast.style.borderLeftColor = type === 'success' ? 'var(--success)' : (type === 'warning' ? 'var(--warning)' : 'var(--primary)');
toast.innerHTML = `
<span>${message}</span>
<button onclick="this.parentElement.remove()" style="background:none;border:none;color:white;cursor:pointer;">✕</button>
`;
container.appendChild(toast);
// Auto remove
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
}
// Initialize Application
const app = new NeuroArchApp();
</script>
</body>
</html>