2026-01-06 22:57:00 -03:00
<!-- Dialog Designer - General Bots (.bas Visual Editor) -->
< style >
/* Designer uses global theme variables with node-specific colors */
.designer-container {
--node-talk: #89b4fa;
--node-hear: #a6e3a1;
--node-set: #f9e2af;
--node-if: #cba6f7;
--node-for: #f38ba8;
--node-call: #94e2d5;
--node-send: #fab387;
--grid-size: 20px;
}
.designer-container {
2025-12-03 18:42:22 -03:00
display: grid;
grid-template-columns: 240px 1fr 280px;
grid-template-rows: 48px 1fr 32px;
2026-01-06 22:57:00 -03:00
height: calc(100vh - 64px);
background: var(--bg, #0f172a);
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
}
/* Header */
.designer-header {
grid-column: 1 / -1;
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border-bottom: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
display: flex;
align-items: center;
padding: 0 16px;
gap: 16px;
}
.designer-logo {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
2026-01-06 22:57:00 -03:00
color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
}
.designer-logo svg {
width: 24px;
height: 24px;
}
.designer-toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.toolbar-btn {
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 1px solid var(--border, #334155);
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
transition: all 0.15s;
}
2025-12-28 11:50:52 -03:00
.toolbar-btn.magic {
2026-01-06 22:57:00 -03:00
background: linear-gradient(135deg, var(--primary, #a855f7), var(--primary, #3b82f6));
border-color: var(--primary, #a855f7);
2025-12-28 11:50:52 -03:00
}
.toolbar-btn.magic:hover {
2026-01-06 22:57:00 -03:00
background: linear-gradient(135deg, var(--primary, #3b82f6), var(--primary, #a855f7));
2025-12-28 11:50:52 -03:00
transform: scale(1.05);
}
.magic-panel {
position: fixed;
right: 20px;
bottom: 60px;
width: 380px;
max-height: 500px;
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border: 1px solid var(--border, #334155);
2025-12-28 11:50:52 -03:00
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
z-index: 1000;
display: none;
flex-direction: column;
overflow: hidden;
}
.magic-panel.visible {
display: flex;
}
.magic-header {
padding: 16px;
2026-01-06 22:57:00 -03:00
border-bottom: 1px solid var(--border, #334155);
2025-12-28 11:50:52 -03:00
display: flex;
align-items: center;
gap: 10px;
background: linear-gradient(135deg, rgba(203,166,247,0.1), rgba(137,180,250,0.1));
}
.magic-header svg {
width: 20px;
height: 20px;
2026-01-06 22:57:00 -03:00
color: var(--primary, #a855f7);
2025-12-28 11:50:52 -03:00
}
.magic-header h3 {
flex: 1;
font-size: 14px;
font-weight: 600;
}
.magic-close {
background: none;
border: none;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-28 11:50:52 -03:00
cursor: pointer;
padding: 4px;
}
.magic-close:hover {
2026-01-06 22:57:00 -03:00
color: var(--text, #f8fafc);
2025-12-28 11:50:52 -03:00
}
.magic-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.magic-loading {
text-align: center;
padding: 40px;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-28 11:50:52 -03:00
}
.magic-loading .spinner {
width: 32px;
height: 32px;
2026-01-06 22:57:00 -03:00
border: 3px solid var(--border, #334155);
border-top-color: var(--primary, #a855f7);
2025-12-28 11:50:52 -03:00
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
.magic-suggestion {
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 1px solid var(--border, #334155);
2025-12-28 11:50:52 -03:00
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.magic-suggestion-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.magic-suggestion-icon {
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
2026-01-06 22:57:00 -03:00
.magic-suggestion-icon.ux { background: rgba(166,227,161,0.2); color: var(--success, #22c55e); }
.magic-suggestion-icon.perf { background: rgba(249,226,175,0.2); color: var(--warning, #f59e0b); }
.magic-suggestion-icon.a11y { background: rgba(137,180,250,0.2); color: var(--primary, #3b82f6); }
.magic-suggestion-icon.feature { background: rgba(203,166,247,0.2); color: var(--primary, #a855f7); }
2025-12-28 11:50:52 -03:00
.magic-suggestion-title {
font-weight: 600;
font-size: 13px;
}
.magic-suggestion-desc {
font-size: 12px;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-28 11:50:52 -03:00
line-height: 1.5;
}
.magic-suggestion-apply {
margin-top: 10px;
2026-01-06 22:57:00 -03:00
background: var(--primary, #a855f7);
2025-12-28 11:50:52 -03:00
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.magic-suggestion-apply:hover {
opacity: 0.9;
}
2025-12-03 18:42:22 -03:00
.toolbar-btn:hover {
2026-01-06 22:57:00 -03:00
background: var(--border, #334155);
2025-12-03 18:42:22 -03:00
}
.toolbar-btn.primary {
2026-01-06 22:57:00 -03:00
background: var(--primary, #3b82f6);
color: var(--bg, #0f172a);
border-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
}
.toolbar-btn.primary:hover {
2026-01-06 22:57:00 -03:00
background: var(--primary-hover, #2563eb);
2025-12-03 18:42:22 -03:00
}
.toolbar-btn svg {
width: 16px;
height: 16px;
}
.toolbar-separator {
width: 1px;
height: 24px;
2026-01-06 22:57:00 -03:00
background: var(--border, #334155);
2025-12-03 18:42:22 -03:00
margin: 0 8px;
}
/* Toolbox Panel */
.toolbox-panel {
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border-right: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
overflow-y: auto;
padding: 12px;
}
.toolbox-section {
margin-bottom: 16px;
}
.toolbox-section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
margin-bottom: 8px;
padding: 0 4px;
}
.toolbox-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 6px;
cursor: grab;
transition: all 0.15s;
margin-bottom: 4px;
border: 1px solid transparent;
}
.toolbox-item:hover {
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
2025-12-03 18:42:22 -03:00
}
.toolbox-item:active {
cursor: grabbing;
}
.toolbox-item.dragging {
opacity: 0.5;
}
.toolbox-icon {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
2026-01-06 22:57:00 -03:00
color: var(--bg, #0f172a);
}
.node-item-name {
font-size: 13px;
font-weight: 500;
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
}
.toolbox-icon.talk { background: var(--node-talk); }
.toolbox-icon.hear { background: var(--node-hear); }
.toolbox-icon.set { background: var(--node-set); }
.toolbox-icon.if { background: var(--node-if); }
.toolbox-icon.for { background: var(--node-for); }
.toolbox-icon.call { background: var(--node-call); }
.toolbox-icon.send { background: var(--node-send); }
2026-01-06 22:57:00 -03:00
.toolbox-icon.get { background: var(--info, #06b6d4); }
.toolbox-icon.wait { background: var(--warning, #f59e0b); }
.toolbox-icon.switch { background: var(--primary, #3b82f6); }
2025-12-03 18:42:22 -03:00
.toolbox-info {
flex: 1;
min-width: 0;
}
.toolbox-name {
font-size: 13px;
font-weight: 500;
margin-bottom: 2px;
}
.toolbox-desc {
font-size: 11px;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Canvas Area */
.canvas-container {
position: relative;
overflow: hidden;
2026-01-06 22:57:00 -03:00
background: var(--bg, #0f172a);
2025-12-03 18:42:22 -03:00
}
.canvas-grid {
position: absolute;
inset: 0;
background-image:
2026-01-06 22:57:00 -03:00
linear-gradient(var(--border, #334155) 1px, transparent 1px),
linear-gradient(90deg, var(--border, #334155) 1px, transparent 1px);
2025-12-03 18:42:22 -03:00
background-size: var(--grid-size) var(--grid-size);
opacity: 0.3;
}
.canvas {
position: absolute;
inset: 0;
overflow: auto;
}
.canvas-inner {
position: relative;
width: 3000px;
height: 2000px;
min-width: 100%;
min-height: 100%;
}
/* Nodes */
.node {
position: absolute;
min-width: 180px;
max-width: 280px;
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border: 2px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
border-radius: 8px;
cursor: move;
user-select: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: box-shadow 0.15s, border-color 0.15s;
}
.node:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.node.selected {
2026-01-06 22:57:00 -03:00
border-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
box-shadow: 0 0 0 3px rgba(137, 180, 250, 0.3);
}
.node-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px 6px 0 0;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
2026-01-06 22:57:00 -03:00
.node-header.talk { background: var(--node-talk); color: var(--bg, #0f172a); }
.node-header.hear { background: var(--node-hear); color: var(--bg, #0f172a); }
.node-header.set { background: var(--node-set); color: var(--bg, #0f172a); }
.node-header.if { background: var(--node-if); color: var(--bg, #0f172a); }
.node-header.for { background: var(--node-for); color: var(--bg, #0f172a); }
.node-header.call { background: var(--node-call); color: var(--bg, #0f172a); }
.node-header.send { background: var(--node-send); color: var(--bg, #0f172a); }
.node-header.get { background: var(--info, #06b6d4); color: var(--bg, #0f172a); }
.node-header.wait { background: var(--warning, #f59e0b); color: var(--bg, #0f172a); }
.node-header.switch { background: var(--primary, #3b82f6); color: white; }
2025-12-03 18:42:22 -03:00
.node-header svg {
width: 14px;
height: 14px;
}
.node-body {
padding: 12px;
}
.node-field {
margin-bottom: 8px;
}
.node-field:last-child {
margin-bottom: 0;
}
.node-field-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
margin-bottom: 4px;
}
.node-field-input {
width: 100%;
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
border-radius: 4px;
padding: 6px 8px;
2026-01-06 22:57:00 -03:00
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
}
.node-field-input:focus {
outline: none;
2026-01-06 22:57:00 -03:00
border-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
}
.node-field-select {
width: 100%;
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
border-radius: 4px;
padding: 6px 8px;
2026-01-06 22:57:00 -03:00
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
font-size: 12px;
cursor: pointer;
}
/* Connection Ports */
.node-port {
position: absolute;
width: 12px;
height: 12px;
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 2px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
border-radius: 50%;
cursor: crosshair;
transition: all 0.15s;
}
.node-port:hover {
2026-01-06 22:57:00 -03:00
background: var(--primary, #3b82f6);
border-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
transform: scale(1.3);
}
.node-port.input {
left: -6px;
top: 50%;
transform: translateY(-50%);
}
.node-port.output {
right: -6px;
top: 50%;
transform: translateY(-50%);
}
.node-port.output-true {
right: -6px;
top: 35%;
2026-01-06 22:57:00 -03:00
background: var(--success, #22c55e);
border-color: var(--success, #22c55e);
2025-12-03 18:42:22 -03:00
}
.node-port.output-false {
right: -6px;
top: 65%;
2026-01-06 22:57:00 -03:00
background: var(--error, #ef4444);
border-color: var(--error, #ef4444);
2025-12-03 18:42:22 -03:00
}
/* Connections SVG */
.connections-layer {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
}
.connection {
fill: none;
2026-01-06 22:57:00 -03:00
stroke: var(--border, #334155);
2025-12-03 18:42:22 -03:00
stroke-width: 2;
pointer-events: stroke;
cursor: pointer;
}
.connection:hover {
2026-01-06 22:57:00 -03:00
stroke: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
stroke-width: 3;
}
.connection.drawing {
2026-01-06 22:57:00 -03:00
stroke: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
stroke-dasharray: 5, 5;
}
/* Properties Panel */
.properties-panel {
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border-left: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
overflow-y: auto;
}
.properties-header {
padding: 12px 16px;
2026-01-06 22:57:00 -03:00
border-bottom: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.properties-header svg {
width: 16px;
height: 16px;
2026-01-06 22:57:00 -03:00
color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
}
.properties-empty {
padding: 24px 16px;
text-align: center;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
font-size: 13px;
}
.properties-content {
padding: 16px;
}
.property-group {
margin-bottom: 16px;
}
.property-group-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
2026-01-06 22:57:00 -03:00
letter-spacing: 0.5px;
color: var(--text-secondary, #94a3b8);
margin-bottom: 12px;
2025-12-03 18:42:22 -03:00
}
.property-field {
margin-bottom: 12px;
}
.property-label {
font-size: 12px;
margin-bottom: 4px;
2026-01-06 22:57:00 -03:00
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
}
.property-input {
width: 100%;
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
border-radius: 4px;
padding: 8px 10px;
2026-01-06 22:57:00 -03:00
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
font-size: 13px;
}
.property-input:focus {
outline: none;
2026-01-06 22:57:00 -03:00
border-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
}
.property-textarea {
min-height: 80px;
resize: vertical;
font-family: 'Consolas', 'Monaco', monospace;
}
.property-checkbox {
display: flex;
align-items: center;
gap: 8px;
}
.property-checkbox input {
width: 16px;
height: 16px;
2026-01-06 22:57:00 -03:00
accent-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
}
/* Status Bar */
.status-bar {
grid-column: 1 / -1;
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border-top: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
display: flex;
align-items: center;
padding: 0 12px;
font-size: 12px;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
gap: 16px;
}
.status-item {
display: flex;
align-items: center;
gap: 6px;
}
.status-item svg {
width: 14px;
height: 14px;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
}
.status-spacer {
flex: 1;
}
.zoom-controls {
display: flex;
align-items: center;
gap: 4px;
}
.zoom-btn {
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
border: 1px solid var(--border, #334155);
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.zoom-btn:hover {
2026-01-06 22:57:00 -03:00
background: var(--border, #334155);
2025-12-03 18:42:22 -03:00
}
.zoom-value {
min-width: 40px;
text-align: center;
}
/* Context Menu */
.context-menu {
position: fixed;
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border: 1px solid var(--border, #334155);
border-radius: 8px;
padding: 8px 0;
2025-12-03 18:42:22 -03:00
min-width: 160px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
z-index: 1000;
display: none;
}
.context-menu.visible {
display: block;
}
.context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.context-menu-item:hover {
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
2025-12-03 18:42:22 -03:00
}
.context-menu-item.danger {
2026-01-06 22:57:00 -03:00
color: var(--error, #ef4444);
2025-12-03 18:42:22 -03:00
}
.context-menu-separator {
height: 1px;
2026-01-06 22:57:00 -03:00
background: var(--border, #334155);
2025-12-03 18:42:22 -03:00
margin: 4px 8px;
}
/* File Browser Modal */
.modal-overlay {
position: fixed;
inset: 0;
2026-01-06 22:57:00 -03:00
background: rgba(0, 0, 0, 0.7);
2025-12-03 18:42:22 -03:00
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
2026-01-06 22:57:00 -03:00
.modal-overlay.show {
2025-12-03 18:42:22 -03:00
display: flex;
}
.modal {
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border: 1px solid var(--border, #334155);
border-radius: 12px;
width: 100%;
max-width: 500px;
2025-12-03 18:42:22 -03:00
max-height: 80vh;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
2026-01-06 22:57:00 -03:00
border-bottom: 1px solid var(--border, #334155);
}
.magic-panel-header {
padding: 14px 16px;
border-bottom: 1px solid var(--border, #334155);
display: flex;
justify-content: space-between;
align-items: center;
2025-12-03 18:42:22 -03:00
}
.modal-title {
font-weight: 600;
font-size: 16px;
}
.modal-close {
background: none;
border: none;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
cursor: pointer;
padding: 4px;
}
.modal-close:hover {
2026-01-06 22:57:00 -03:00
color: var(--text, #f8fafc);
2025-12-03 18:42:22 -03:00
}
.modal-body {
padding: 20px;
overflow-y: auto;
max-height: 400px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 16px 20px;
2026-01-06 22:57:00 -03:00
border-top: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
}
.file-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.file-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.15s;
}
.file-item:hover {
2026-01-06 22:57:00 -03:00
background: var(--surface-hover, #334155);
2025-12-03 18:42:22 -03:00
}
.file-item.selected {
2026-01-06 22:57:00 -03:00
background: var(--primary, #3b82f6);
color: var(--bg, #0f172a);
2025-12-03 18:42:22 -03:00
}
.file-icon {
width: 20px;
height: 20px;
}
.file-name {
flex: 1;
font-size: 14px;
}
.file-path {
font-size: 11px;
2026-01-06 22:57:00 -03:00
color: var(--text-secondary, #94a3b8);
2025-12-03 18:42:22 -03:00
}
.file-item.selected .file-path {
2026-01-06 22:57:00 -03:00
color: var(--surface-hover, #334155);
2025-12-03 18:42:22 -03:00
}
/* HTMX indicators */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-block;
}
.htmx-request.htmx-indicator {
display: inline-block;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 16px;
height: 16px;
2026-01-06 22:57:00 -03:00
border: 2px solid var(--border, #334155);
border-top-color: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Minimap */
.minimap {
position: absolute;
bottom: 16px;
right: 16px;
2026-01-06 22:57:00 -03:00
width: 150px;
2025-12-03 18:42:22 -03:00
height: 100px;
2026-01-06 22:57:00 -03:00
background: var(--surface, #1e293b);
border: 1px solid var(--border, #334155);
2025-12-03 18:42:22 -03:00
border-radius: 6px;
overflow: hidden;
}
.minimap-viewport {
position: absolute;
2026-01-06 22:57:00 -03:00
border: 1px solid var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
background: rgba(137, 180, 250, 0.1);
}
.minimap-node {
position: absolute;
2026-01-06 22:57:00 -03:00
background: var(--primary, #3b82f6);
2025-12-03 18:42:22 -03:00
border-radius: 1px;
}
2026-01-06 22:57:00 -03:00
< / style >
<!-- Note: ws - connect="/ws/designer" removed until backend WebSocket endpoint is implemented -->
< div class = "designer-container" >
2025-12-03 18:42:22 -03:00
<!-- Header -->
< header class = "designer-header" >
< div class = "designer-logo" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M12 2L2 7l10 5 10-5-10-5z" / >
< path d = "M2 17l10 5 10-5" / >
< path d = "M2 12l10 5 10-5" / >
< / svg >
< span > Dialog Designer< / span >
< / div >
< div class = "designer-toolbar" >
< button class = "toolbar-btn" id = "btn-new" title = "New Dialog (Ctrl+N)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / >
< polyline points = "14 2 14 8 20 8" / >
< line x1 = "12" y1 = "18" x2 = "12" y2 = "12" / >
< line x1 = "9" y1 = "15" x2 = "15" y2 = "15" / >
< / svg >
New
< / button >
< button class = "toolbar-btn" id = "btn-open"
2026-01-10 10:54:05 -03:00
hx-get="/api/designer/files"
2025-12-03 18:42:22 -03:00
hx-target="#file-list-content"
hx-trigger="click"
onclick="showModal('open-modal')"
title="Open Dialog (Ctrl+O)">
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" / >
< / svg >
Open
< / button >
< button class = "toolbar-btn primary" id = "btn-save"
2026-01-10 10:54:05 -03:00
hx-post="/api/designer/save"
2025-12-03 18:42:22 -03:00
hx-include="#designer-data"
hx-indicator="#save-spinner"
title="Save (Ctrl+S)">
< span class = "htmx-indicator spinner" id = "save-spinner" > < / span >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" / >
< polyline points = "17 21 17 13 7 13 7 21" / >
< polyline points = "7 3 7 8 15 8" / >
< / svg >
Save
< / button >
< div class = "toolbar-separator" > < / div >
< button class = "toolbar-btn" id = "btn-undo" title = "Undo (Ctrl+Z)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M3 7v6h6" / >
< path d = "M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" / >
< / svg >
< / button >
< button class = "toolbar-btn" id = "btn-redo" title = "Redo (Ctrl+Y)" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M21 7v6h-6" / >
< path d = "M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13" / >
< / svg >
< / button >
< div class = "toolbar-separator" > < / div >
< button class = "toolbar-btn" id = "btn-run"
2026-01-10 10:54:05 -03:00
hx-post="/api/designer/validate"
2025-12-03 18:42:22 -03:00
hx-include="#designer-data"
title="Validate Dialog">
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< polygon points = "5 3 19 12 5 21 5 3" / >
< / svg >
Validate
< / button >
< button class = "toolbar-btn" id = "btn-export"
2026-01-10 10:54:05 -03:00
hx-get="/api/designer/export"
2025-12-03 18:42:22 -03:00
hx-include="#designer-data"
title="Export as .bas">
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / >
< polyline points = "7 10 12 15 17 10" / >
< line x1 = "12" y1 = "15" x2 = "12" y2 = "3" / >
< / svg >
Export
< / button >
2025-12-28 11:50:52 -03:00
< div class = "toolbar-separator" > < / div >
< button class = "toolbar-btn magic" id = "btn-magic"
onclick="showMagicPanel()"
title="AI Improvements (Ctrl+M)">
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" / >
< / svg >
Magic
< / button >
2025-12-03 18:42:22 -03:00
< / div >
< / header >
<!-- Toolbox Panel -->
< aside class = "toolbox-panel" >
< div class = "toolbox-section" >
< div class = "toolbox-section-title" > Conversation< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "TALK" >
< div class = "toolbox-icon talk" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > TALK< / div >
< div class = "toolbox-desc" > Send message to user< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "HEAR" >
< div class = "toolbox-icon hear" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" / >
< path d = "M19 10v2a7 7 0 0 1-14 0v-2" / >
< line x1 = "12" y1 = "19" x2 = "12" y2 = "23" / >
< line x1 = "8" y1 = "23" x2 = "16" y2 = "23" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > HEAR< / div >
< div class = "toolbox-desc" > Wait for user input< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "WAIT" >
< div class = "toolbox-icon wait" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< circle cx = "12" cy = "12" r = "10" / >
< polyline points = "12 6 12 12 16 14" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > WAIT< / div >
< div class = "toolbox-desc" > Pause execution< / div >
< / div >
< / div >
< / div >
< div class = "toolbox-section" >
< div class = "toolbox-section-title" > Logic< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "IF" >
< div class = "toolbox-icon if" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< polyline points = "9 18 15 12 9 6" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > IF< / div >
< div class = "toolbox-desc" > Conditional branch< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "SWITCH" >
< div class = "toolbox-icon switch" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M18 8L22 12L18 16" / >
< path d = "M2 12H22" / >
< path d = "M6 8L2 12L6 16" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > SWITCH< / div >
< div class = "toolbox-desc" > Multiple conditions< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "FOR" >
< div class = "toolbox-icon for" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< polyline points = "23 4 23 10 17 10" / >
< polyline points = "1 20 1 14 7 14" / >
< path d = "M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > FOR EACH< / div >
< div class = "toolbox-desc" > Loop through items< / div >
< / div >
< / div >
< / div >
< div class = "toolbox-section" >
< div class = "toolbox-section-title" > Data< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "SET" >
< div class = "toolbox-icon set" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< polyline points = "4 7 4 4 20 4 20 7" / >
< line x1 = "9" y1 = "20" x2 = "15" y2 = "20" / >
< line x1 = "12" y1 = "4" x2 = "12" y2 = "20" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > SET< / div >
< div class = "toolbox-desc" > Assign variable< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "GET" >
< div class = "toolbox-icon get" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< circle cx = "12" cy = "12" r = "3" / >
< path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > GET< / div >
< div class = "toolbox-desc" > Fetch data< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "SAVE" >
< div class = "toolbox-icon send" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" / >
< polyline points = "17 21 17 13 7 13 7 21" / >
< polyline points = "7 3 7 8 15 8" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > SAVE< / div >
< div class = "toolbox-desc" > Save to storage< / div >
< / div >
< / div >
< / div >
< div class = "toolbox-section" >
< div class = "toolbox-section-title" > Actions< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "SEND MAIL" >
< div class = "toolbox-icon send" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" / >
< polyline points = "22,6 12,13 2,6" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > SEND MAIL< / div >
< div class = "toolbox-desc" > Send email< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "CALL" >
< div class = "toolbox-icon call" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< rect x = "3" y = "3" width = "18" height = "18" rx = "2" ry = "2" / >
< line x1 = "9" y1 = "9" x2 = "15" y2 = "15" / >
< line x1 = "15" y1 = "9" x2 = "9" y2 = "15" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > CALL< / div >
< div class = "toolbox-desc" > Call procedure< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "POST" >
< div class = "toolbox-icon call" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< circle cx = "12" cy = "12" r = "10" / >
< line x1 = "2" y1 = "12" x2 = "22" y2 = "12" / >
< path d = "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > POST< / div >
< div class = "toolbox-desc" > HTTP POST request< / div >
< / div >
< / div >
< / div >
< div class = "toolbox-section" >
< div class = "toolbox-section-title" > Memory< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "SET BOT MEMORY" >
< div class = "toolbox-icon set" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< rect x = "2" y = "2" width = "20" height = "8" rx = "2" ry = "2" / >
< rect x = "2" y = "14" width = "20" height = "8" rx = "2" ry = "2" / >
< line x1 = "6" y1 = "6" x2 = "6.01" y2 = "6" / >
< line x1 = "6" y1 = "18" x2 = "6.01" y2 = "18" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > SET BOT MEMORY< / div >
< div class = "toolbox-desc" > Store bot data< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "GET BOT MEMORY" >
< div class = "toolbox-icon get" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< rect x = "2" y = "2" width = "20" height = "8" rx = "2" ry = "2" / >
< rect x = "2" y = "14" width = "20" height = "8" rx = "2" ry = "2" / >
< line x1 = "6" y1 = "6" x2 = "6.01" y2 = "6" / >
< line x1 = "6" y1 = "18" x2 = "6.01" y2 = "18" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > GET BOT MEMORY< / div >
< div class = "toolbox-desc" > Retrieve bot data< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "SET USER MEMORY" >
< div class = "toolbox-icon set" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / >
< circle cx = "12" cy = "7" r = "4" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > SET USER MEMORY< / div >
< div class = "toolbox-desc" > Store user data< / div >
< / div >
< / div >
< div class = "toolbox-item" draggable = "true" data-node-type = "GET USER MEMORY" >
< div class = "toolbox-icon get" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / >
< circle cx = "12" cy = "7" r = "4" / >
< / svg >
< / div >
< div class = "toolbox-info" >
< div class = "toolbox-name" > GET USER MEMORY< / div >
< div class = "toolbox-desc" > Retrieve user data< / div >
< / div >
< / div >
< / div >
< / aside >
<!-- Canvas Area -->
< div class = "canvas-container" id = "canvas-container" >
< div class = "canvas-grid" > < / div >
< div class = "canvas" id = "canvas" >
< svg class = "connections-layer" id = "connections-layer" >
< defs >
< marker id = "arrowhead" markerWidth = "10" markerHeight = "7" refX = "9" refY = "3.5" orient = "auto" >
2026-01-06 22:57:00 -03:00
< polygon points = "0 0, 10 3.5, 0 7" fill = "var(--border, #334155)" / >
2025-12-03 18:42:22 -03:00
< / marker >
< / defs >
< / svg >
< div class = "canvas-inner" id = "canvas-inner" >
<!-- Nodes are rendered here dynamically -->
< / div >
< / div >
<!-- Minimap -->
< div class = "minimap" id = "minimap" >
< div class = "minimap-viewport" id = "minimap-viewport" > < / div >
< / div >
< / div >
2025-12-28 11:50:52 -03:00
<!-- Magic AI Panel -->
< div class = "magic-panel" id = "magic-panel" >
< div class = "magic-header" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" / >
< / svg >
< h3 > AI Improvements< / h3 >
< button class = "magic-close" onclick = "hideMagicPanel()" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / >
< line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / >
< / svg >
< / button >
< / div >
< div class = "magic-content" id = "magic-content" >
< div class = "magic-loading" >
< div class = "spinner" > < / div >
< p > Analyzing your dialog...< / p >
< / div >
< / div >
< / div >
2025-12-03 18:42:22 -03:00
<!-- Properties Panel -->
< aside class = "properties-panel" id = "properties-panel" >
< div class = "properties-header" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< circle cx = "12" cy = "12" r = "3" / >
< path d = "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" / >
< / svg >
Properties
< / div >
< div class = "properties-content" id = "properties-content" >
< div class = "properties-empty" >
Select a node to edit its properties
< / div >
< / div >
< / aside >
<!-- Status Bar -->
< footer class = "status-bar" >
< div class = "status-item" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" >
< path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" / >
< polyline points = "14 2 14 8 20 8" / >
< / svg >
< span id = "file-name" > Untitled.bas< / span >
< / div >
< div class = "status-item" >
< span id = "node-count" > 0 nodes< / span >
< / div >
< div class = "status-item" >
< span id = "connection-count" > 0 connections< / span >
< / div >
< div class = "status-spacer" > < / div >
< div class = "status-item" id = "save-status" >
< span > Not saved< / span >
< / div >
< div class = "zoom-controls" >
< button class = "zoom-btn" id = "zoom-out" title = "Zoom Out" > − < / button >
< span class = "zoom-value" id = "zoom-value" > 100%< / span >
< button < / span > class="zoom-btn" id="zoom-in" title="Zoom In">+< / button >
< / div >
< / footer >
< / div >
<!-- Context Menu -->
< div class = "context-menu" id = "context-menu" >
< div class = "context-menu-item" data-action = "duplicate" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" / >
< path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" / >
< / svg >
Duplicate
< / div >
< div class = "context-menu-item" data-action = "copy" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< rect x = "9" y = "9" width = "13" height = "13" rx = "2" ry = "2" / >
< path d = "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" / >
< / svg >
Copy
< / div >
< div class = "context-menu-separator" > < / div >
< div class = "context-menu-item" data-action = "bring-front" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< polyline points = "17 11 12 6 7 11" / >
< polyline points = "17 18 12 13 7 18" / >
< / svg >
Bring to Front
< / div >
< div class = "context-menu-item" data-action = "send-back" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< polyline points = "7 13 12 18 17 13" / >
< polyline points = "7 6 12 11 17 6" / >
< / svg >
Send to Back
< / div >
< div class = "context-menu-separator" > < / div >
< div class = "context-menu-item danger" data-action = "delete" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
< polyline points = "3 6 5 6 21 6" / >
< path d = "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" / >
< / svg >
Delete
< / div >
< / div >
<!-- Open File Modal -->
< div class = "modal-overlay" id = "open-modal" >
< div class = "modal" >
< div class = "modal-header" >
< span class = "modal-title" > Open Dialog File< / span >
< button class = "modal-close" onclick = "hideModal('open-modal')" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "20" height = "20" >
< line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / >
< line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / >
< / svg >
< / button >
< / div >
< div class = "modal-body" >
< div class = "file-list" id = "file-list-content" >
< div class = "htmx-indicator" style = "text-align: center; padding: 20px;" >
< div class = "spinner" style = "margin: 0 auto;" > < / div >
2026-01-06 22:57:00 -03:00
< div style = "margin-top: 8px; color: var(--text-secondary, #94a3b8);" > Loading files...< / div >
2025-12-03 18:42:22 -03:00
< / div >
< / div >
< / div >
< div class = "modal-footer" >
< button class = "toolbar-btn" onclick = "hideModal('open-modal')" > Cancel< / button >
< button class = "toolbar-btn primary" id = "btn-open-selected"
2026-01-10 10:54:05 -03:00
hx-get="/api/designer/load"
2025-12-03 18:42:22 -03:00
hx-include="#selected-file"
hx-target="#canvas-inner"
hx-swap="innerHTML"
onclick="hideModal('open-modal')">
Open
< / button >
< / div >
< / div >
< / div >
<!-- Hidden form for designer data -->
< form id = "designer-data" style = "display: none;" >
< input type = "hidden" name = "filename" id = "current-filename" value = "" >
< textarea name = "nodes" id = "nodes-data" > < / textarea >
< textarea name = "connections" id = "connections-data" > < / textarea >
< / form >
< input type = "hidden" id = "selected-file" name = "path" value = "" >
< script >
// Designer State
const state = {
nodes: new Map(),
connections: [],
selectedNode: null,
selectedConnection: null,
isDragging: false,
isConnecting: false,
connectionStart: null,
zoom: 1,
pan: { x: 0, y: 0 },
history: [],
historyIndex: -1,
clipboard: null,
2026-01-06 22:57:00 -03:00
nextNodeId: 1,
driveSource: null
2025-12-03 18:42:22 -03:00
};
// Node Templates
const nodeTemplates = {
'TALK': {
fields: [
{ name: 'message', label: 'Message', type: 'textarea', default: 'Hello!' }
],
hasInput: true,
hasOutput: true
},
'HEAR': {
fields: [
{ name: 'variable', label: 'Variable', type: 'text', default: 'response' },
{ name: 'type', label: 'Type', type: 'select', options: ['STRING', 'NUMBER', 'DATE', 'EMAIL', 'PHONE'], default: 'STRING' }
],
hasInput: true,
hasOutput: true
},
'SET': {
fields: [
{ name: 'variable', label: 'Variable', type: 'text', default: 'value' },
{ name: 'expression', label: 'Expression', type: 'text', default: '' }
],
hasInput: true,
hasOutput: true
},
'IF': {
fields: [
{ name: 'condition', label: 'Condition', type: 'text', default: 'value = 1' }
],
hasInput: true,
hasOutput: false,
hasOutputTrue: true,
hasOutputFalse: true
},
'FOR': {
fields: [
{ name: 'variable', label: 'Item Variable', type: 'text', default: 'item' },
{ name: 'collection', label: 'Collection', type: 'text', default: 'items' }
],
hasInput: true,
hasOutput: true,
hasLoopOutput: true
},
'SWITCH': {
fields: [
{ name: 'expression', label: 'Expression', type: 'text', default: 'value' }
],
hasInput: true,
hasOutput: true
},
'CALL': {
fields: [
{ name: 'procedure', label: 'Procedure', type: 'text', default: '' },
{ name: 'arguments', label: 'Arguments', type: 'text', default: '' }
],
hasInput: true,
hasOutput: true
},
'SEND MAIL': {
fields: [
{ name: 'to', label: 'To', type: 'text', default: '' },
{ name: 'subject', label: 'Subject', type: 'text', default: '' },
{ name: 'body', label: 'Body', type: 'textarea', default: '' }
],
hasInput: true,
hasOutput: true
},
'GET': {
fields: [
{ name: 'url', label: 'URL', type: 'text', default: '' },
{ name: 'variable', label: 'Result Variable', type: 'text', default: 'result' }
],
hasInput: true,
hasOutput: true
},
'POST': {
fields: [
{ name: 'url', label: 'URL', type: 'text', default: '' },
{ name: 'body', label: 'Body', type: 'textarea', default: '' },
{ name: 'variable', label: 'Result Variable', type: 'text', default: 'result' }
],
hasInput: true,
hasOutput: true
},
'SAVE': {
fields: [
{ name: 'filename', label: 'Filename', type: 'text', default: 'data.csv' },
{ name: 'data', label: 'Data', type: 'text', default: '' }
],
hasInput: true,
hasOutput: true
},
'WAIT': {
fields: [
{ name: 'duration', label: 'Duration (seconds)', type: 'text', default: '5' }
],
hasInput: true,
hasOutput: true
},
'SET BOT MEMORY': {
fields: [
{ name: 'key', label: 'Key', type: 'text', default: '' },
{ name: 'value', label: 'Value', type: 'text', default: '' }
],
hasInput: true,
hasOutput: true
},
'GET BOT MEMORY': {
fields: [
{ name: 'key', label: 'Key', type: 'text', default: '' },
{ name: 'variable', label: 'Variable', type: 'text', default: 'value' }
],
hasInput: true,
hasOutput: true
},
'SET USER MEMORY': {
fields: [
{ name: 'key', label: 'Key', type: 'text', default: '' },
{ name: 'value', label: 'Value', type: 'text', default: '' }
],
hasInput: true,
hasOutput: true
},
'GET USER MEMORY': {
fields: [
{ name: 'key', label: 'Key', type: 'text', default: '' },
{ name: 'variable', label: 'Variable', type: 'text', default: 'value' }
],
hasInput: true,
hasOutput: true
}
};
// Initialize
2026-01-06 22:57:00 -03:00
function initDesigner() {
console.log('initDesigner called');
2025-12-03 18:42:22 -03:00
initDragAndDrop();
initCanvasInteraction();
initKeyboardShortcuts();
initContextMenu();
updateStatusBar();
2026-01-06 22:57:00 -03:00
loadFromUrlParams();
}
// Run on DOMContentLoaded (for direct page load)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDesigner);
} else {
// DOM already loaded (HTMX injection case)
initDesigner();
}
// Also run when HTMX swaps content
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.target.id === 'main-content') {
console.log('htmx:afterSwap detected for main-content');
initDesigner();
}
2025-12-03 18:42:22 -03:00
});
2026-01-06 22:57:00 -03:00
// Load file from URL parameters (when opening .bas from drive)
async function loadFromUrlParams() {
// Parameters can be in query string OR in hash fragment (after #designer?)
let bucket = null;
let path = null;
// First try query string
const queryParams = new URLSearchParams(window.location.search);
bucket = queryParams.get('bucket');
path = queryParams.get('path');
// If not found, try hash fragment (e.g., /#designer?bucket=x& path=y)
if (!bucket || !path) {
const hash = window.location.hash;
const hashQueryIndex = hash.indexOf('?');
if (hashQueryIndex !== -1) {
const hashParams = new URLSearchParams(hash.substring(hashQueryIndex + 1));
bucket = bucket || hashParams.get('bucket');
path = path || hashParams.get('path');
}
}
console.log('loadFromUrlParams called:', { bucket, path, hash: window.location.hash, search: window.location.search });
if (bucket & & path) {
const fileName = path.split('/').pop() || 'dialog.bas';
document.getElementById('current-filename').value = path;
document.getElementById('selected-file').value = path;
state.driveSource = { bucket, path };
try {
// Fetch file content directly from drive API
const response = await fetch('/api/files/read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bucket, path })
});
if (!response.ok) {
throw new Error(`Failed to load file: ${response.statusText}`);
}
const data = await response.json();
const content = data.content || '';
console.log('Loaded .bas content:', content.substring(0, 200) + '...');
// Parse BASIC code and create nodes
parseBasicCodeToNodes(content);
updateStatusBar();
const statusEl = document.querySelector('.status-item span');
if (statusEl) {
statusEl.textContent = `Loaded: ${fileName}`;
}
} catch (err) {
console.error('Failed to load .bas file:', err);
alert(`Failed to load file: ${err.message}`);
}
}
}
// Parse BASIC code and create visual nodes
function parseBasicCodeToNodes(content) {
console.log('parseBasicCodeToNodes called');
state.nodes.clear();
state.connections = [];
state.nextNodeId = 1;
const lines = content.split('\n');
let yPos = 100;
let nodeCount = 0;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("'")) continue;
const upper = trimmed.toUpperCase();
let nodeType = null;
let fields = {};
if (upper.startsWith('TALK ')) {
nodeType = 'TALK';
const match = trimmed.match(/TALK\s+"([^"]*)"/i) || trimmed.match(/TALK\s+(.+)/i);
fields.message = match ? match[1] : '';
} else if (upper.startsWith('HEAR ')) {
nodeType = 'HEAR';
const match = trimmed.match(/HEAR\s+(\w+)(?:\s+AS\s+(\w+))?/i);
fields.variable = match ? match[1] : 'input';
fields.type = match & & match[2] ? match[2] : 'string';
} else if (upper.startsWith('SET ') || upper.includes(' = ')) {
nodeType = 'SET';
const match = trimmed.match(/(?:SET\s+)?(\w+)\s*=\s*(.+)/i);
fields.variable = match ? match[1] : 'x';
fields.expression = match ? match[2] : '0';
} else if (upper.startsWith('IF ')) {
nodeType = 'IF';
const match = trimmed.match(/IF\s+(.+?)\s+THEN/i);
fields.condition = match ? match[1] : 'true';
} else if (upper.startsWith('FOR ')) {
nodeType = 'FOR';
const match = trimmed.match(/FOR\s+(?:EACH\s+)?(\w+)\s+IN\s+(.+)/i);
fields.variable = match ? match[1] : 'item';
fields.collection = match ? match[2] : 'items';
} else if (upper.startsWith('CALL ')) {
nodeType = 'CALL';
const match = trimmed.match(/CALL\s+(\w+)\s*\(([^)]*)\)/i);
fields.procedure = match ? match[1] : 'sub';
fields.arguments = match ? match[2] : '';
} else if (upper.startsWith('WAIT ')) {
nodeType = 'WAIT';
const match = trimmed.match(/WAIT\s+(\d+)/i);
fields.duration = match ? match[1] : '1000';
} else if (upper.startsWith('GET ')) {
nodeType = 'GET';
const match = trimmed.match(/GET\s+(.+?)\s+TO\s+(\w+)/i);
fields.url = match ? match[1] : '';
fields.variable = match ? match[2] : 'result';
} else if (upper.startsWith('PARAM ')) {
nodeType = 'HEAR';
const match = trimmed.match(/PARAM\s+(\w+)\s+AS\s+(\w+)/i);
fields.variable = match ? match[1] : 'param';
fields.type = match ? match[2] : 'string';
}
if (nodeType & & nodeTemplates[nodeType]) {
const node = createNode(nodeType, 400, yPos);
if (node) {
Object.assign(node.fields, fields);
// Update the rendered node with field values
const nodeEl = document.getElementById(node.id);
if (nodeEl) {
nodeEl.querySelectorAll('.node-field-input, .node-field-select, textarea').forEach(input => {
const fieldName = input.dataset.field || input.name;
if (fields[fieldName] !== undefined) {
input.value = fields[fieldName];
}
});
}
yPos += 100;
nodeCount++;
console.log('Created node:', nodeType, fields);
}
}
}
console.log(`Parsed ${nodeCount} nodes from BASIC code`);
updateStatusBar();
saveToHistory();
}
// Initialize canvas with loaded nodes from server (called by HTMX response)
function initializeCanvas() {
console.log('initializeCanvas called');
const canvasLoaded = document.querySelector('.canvas-loaded');
if (!canvasLoaded) {
console.log('No canvas-loaded element found');
return;
}
const content = canvasLoaded.dataset.content || '';
console.log('Canvas content from server:', content.substring(0, 100));
// Remove the server-rendered container and parse content client-side
canvasLoaded.remove();
parseBasicCodeToNodes(content);
}
2025-12-03 18:42:22 -03:00
// Drag and Drop from Toolbox
function initDragAndDrop() {
const toolboxItems = document.querySelectorAll('.toolbox-item');
const canvas = document.getElementById('canvas-inner');
toolboxItems.forEach(item => {
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('nodeType', item.dataset.nodeType);
item.classList.add('dragging');
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
});
});
canvas.addEventListener('dragover', (e) => {
e.preventDefault();
});
canvas.addEventListener('drop', (e) => {
e.preventDefault();
const nodeType = e.dataTransfer.getData('nodeType');
if (nodeType) {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / state.zoom;
const y = (e.clientY - rect.top) / state.zoom;
createNode(nodeType, snapToGrid(x), snapToGrid(y));
}
});
}
// Canvas Interaction
function initCanvasInteraction() {
const canvas = document.getElementById('canvas');
const container = document.getElementById('canvas-container');
// Pan with middle mouse or space+drag
let isPanning = false;
let panStart = { x: 0, y: 0 };
canvas.addEventListener('mousedown', (e) => {
if (e.button === 1 || (e.button === 0 & & e.target === canvas)) {
isPanning = true;
panStart = { x: e.clientX - state.pan.x, y: e.clientY - state.pan.y };
canvas.style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', (e) => {
if (isPanning) {
state.pan.x = e.clientX - panStart.x;
state.pan.y = e.clientY - panStart.y;
updateCanvasTransform();
}
});
document.addEventListener('mouseup', () => {
isPanning = false;
canvas.style.cursor = 'default';
});
// Zoom with scroll
container.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
const newZoom = Math.min(Math.max(state.zoom + delta, 0.25), 2);
state.zoom = newZoom;
updateCanvasTransform();
updateZoomDisplay();
});
}
function updateCanvasTransform() {
const inner = document.getElementById('canvas-inner');
inner.style.transform = `translate(${state.pan.x}px, ${state.pan.y}px) scale(${state.zoom})`;
}
function updateZoomDisplay() {
document.getElementById('zoom-value').textContent = Math.round(state.zoom * 100) + '%';
}
// Grid snapping
function snapToGrid(value, gridSize = 20) {
return Math.round(value / gridSize) * gridSize;
}
// Create Node
function createNode(type, x, y) {
const template = nodeTemplates[type];
2026-01-06 22:57:00 -03:00
if (!template) {
console.warn('No template found for node type:', type);
return null;
}
2025-12-03 18:42:22 -03:00
const id = 'node-' + state.nextNodeId++;
const node = {
id,
type,
x,
y,
fields: {}
};
// Initialize field values
template.fields.forEach(field => {
node.fields[field.name] = field.default;
});
state.nodes.set(id, node);
renderNode(node);
saveToHistory();
updateStatusBar();
2026-01-06 22:57:00 -03:00
return node;
2025-12-03 18:42:22 -03:00
}
// Render Node
function renderNode(node) {
const template = nodeTemplates[node.type];
const typeClass = node.type.toLowerCase().replace(/\s+/g, '-');
let fieldsHtml = '';
template.fields.forEach(field => {
const value = node.fields[field.name] || '';
if (field.type === 'textarea') {
fieldsHtml += `
< div class = "node-field" >
< label class = "node-field-label" > ${field.label}< / label >
< textarea class = "node-field-input" data-field = "${field.name}" rows = "2" > ${value}< / textarea >
< / div >
`;
} else if (field.type === 'select') {
const options = field.options.map(opt =>
`< option value = "${opt}" $ { opt = == value ? ' selected ' : ' ' } > ${opt}< / option > `
).join('');
fieldsHtml += `
< div class = "node-field" >
< label class = "node-field-label" > ${field.label}< / label >
< select class = "node-field-select" data-field = "${field.name}" > ${options}< / select >
< / div >
`;
} else {
fieldsHtml += `
< div class = "node-field" >
< label class = "node-field-label" > ${field.label}< / label >
< input type = "text" class = "node-field-input" data-field = "${field.name}" value = "${value}" >
< / div >
`;
}
});
let portsHtml = '';
if (template.hasInput) {
portsHtml += `< div class = "node-port input" data-port = "input" > < / div > `;
}
if (template.hasOutput) {
portsHtml += `< div class = "node-port output" data-port = "output" > < / div > `;
}
if (template.hasOutputTrue) {
portsHtml += `< div class = "node-port output-true" data-port = "true" title = "True" > < / div > `;
}
if (template.hasOutputFalse) {
portsHtml += `< div class = "node-port output-false" data-port = "false" title = "False" > < / div > `;
}
const nodeEl = document.createElement('div');
nodeEl.className = 'node';
nodeEl.id = node.id;
nodeEl.style.left = node.x + 'px';
nodeEl.style.top = node.y + 'px';
nodeEl.innerHTML = `
< div class = "node-header ${typeClass}" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "16" height = "16" >
${getNodeIcon(node.type)}
< / svg >
${node.type}
< / div >
< div class = "node-body" >
${fieldsHtml}
< / div >
${portsHtml}
`;
// Make draggable
nodeEl.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('node-port')) return;
selectNode(node.id);
startNodeDrag(e, node);
});
// Field change handlers
nodeEl.querySelectorAll('.node-field-input, .node-field-select').forEach(input => {
input.addEventListener('change', (e) => {
node.fields[e.target.dataset.field] = e.target.value;
saveToHistory();
});
});
// Port handlers for connections
nodeEl.querySelectorAll('.node-port').forEach(port => {
port.addEventListener('mousedown', (e) => {
e.stopPropagation();
startConnection(node.id, port.dataset.port);
});
port.addEventListener('mouseup', (e) => {
e.stopPropagation();
endConnection(node.id, port.dataset.port);
});
});
document.getElementById('canvas-inner').appendChild(nodeEl);
}
function getNodeIcon(type) {
const icons = {
'TALK': '< path d = "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" / > ',
'HEAR': '< path d = "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" / > < path d = "M19 10v2a7 7 0 0 1-14 0v-2" / > < line x1 = "12" y1 = "19" x2 = "12" y2 = "23" / > ',
'SET': '< polyline points = "4 17 10 11 4 5" / > < line x1 = "12" y1 = "19" x2 = "20" y2 = "19" / > ',
'IF': '< path d = "M16 3h5v5" / > < path d = "M8 3H3v5" / > < path d = "M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3" / > < path d = "m15 9 6-6" / > ',
'FOR': '< polyline points = "17 1 21 5 17 9" / > < path d = "M3 11V9a4 4 0 0 1 4-4h14" / > < polyline points = "7 23 3 19 7 15" / > < path d = "M21 13v2a4 4 0 0 1-4 4H3" / > ',
'SWITCH': '< path d = "M18 20V10" / > < path d = "M12 20V4" / > < path d = "M6 20v-6" / > ',
'CALL': '< path d = "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.362 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.338 1.85.573 2.81.7A2 2 0 0 1 22 16.92z" / > ',
'SEND MAIL': '< path d = "M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" / > < polyline points = "22,6 12,13 2,6" / > ',
'GET': '< path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "7 10 12 15 17 10" / > < line x1 = "12" y1 = "15" x2 = "12" y2 = "3" / > ',
'POST': '< path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "17 8 12 3 7 8" / > < line x1 = "12" y1 = "3" x2 = "12" y2 = "15" / > ',
'SAVE': '< path d = "M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" / > < polyline points = "17 21 17 13 7 13 7 21" / > < polyline points = "7 3 7 8 15 8" / > ',
'WAIT': '< circle cx = "12" cy = "12" r = "10" / > < polyline points = "12 6 12 12 16 14" / > ',
'SET BOT MEMORY': '< path d = "M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" / > < line x1 = "12" y1 = "2" x2 = "12" y2 = "6" / > < line x1 = "12" y1 = "18" x2 = "12" y2 = "22" / > ',
'GET BOT MEMORY': '< path d = "M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" / > < polyline points = "7 10 12 15 17 10" / > ',
'SET USER MEMORY': '< path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > ',
'GET USER MEMORY': '< path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" / > < circle cx = "12" cy = "7" r = "4" / > '
};
return icons[type] || '< circle cx = "12" cy = "12" r = "10" / > ';
}
// Node dragging
function startNodeDrag(e, node) {
state.isDragging = true;
const nodeEl = document.getElementById(node.id);
const startX = e.clientX;
const startY = e.clientY;
const origX = node.x;
const origY = node.y;
function onMove(e) {
const dx = (e.clientX - startX) / state.zoom;
const dy = (e.clientY - startY) / state.zoom;
node.x = snapToGrid(origX + dx);
node.y = snapToGrid(origY + dy);
nodeEl.style.left = node.x + 'px';
nodeEl.style.top = node.y + 'px';
updateConnections();
}
function onUp() {
state.isDragging = false;
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
saveToHistory();
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
// Node selection
function selectNode(id) {
if (state.selectedNode) {
const prevEl = document.getElementById(state.selectedNode);
if (prevEl) prevEl.classList.remove('selected');
}
state.selectedNode = id;
const nodeEl = document.getElementById(id);
if (nodeEl) nodeEl.classList.add('selected');
updatePropertiesPanel();
}
function deselectAll() {
if (state.selectedNode) {
const el = document.getElementById(state.selectedNode);
if (el) el.classList.remove('selected');
}
state.selectedNode = null;
state.selectedConnection = null;
updatePropertiesPanel();
}
// Connections
function startConnection(nodeId, portType) {
state.isConnecting = true;
state.connectionStart = { nodeId, portType };
}
function endConnection(nodeId, portType) {
if (!state.isConnecting || !state.connectionStart) return;
if (state.connectionStart.nodeId === nodeId) {
state.isConnecting = false;
state.connectionStart = null;
return;
}
// Only allow output to input connections
if (state.connectionStart.portType === 'input' || portType !== 'input') {
state.isConnecting = false;
state.connectionStart = null;
return;
}
const connection = {
from: state.connectionStart.nodeId,
fromPort: state.connectionStart.portType,
to: nodeId,
toPort: portType
};
state.connections.push(connection);
state.isConnecting = false;
state.connectionStart = null;
updateConnections();
saveToHistory();
}
function updateConnections() {
const svg = document.getElementById('connections-svg');
let paths = '';
state.connections.forEach((conn, index) => {
const fromEl = document.getElementById(conn.from);
const toEl = document.getElementById(conn.to);
if (!fromEl || !toEl) return;
const fromPort = fromEl.querySelector(`[data-port="${conn.fromPort}"]`);
const toPort = toEl.querySelector(`[data-port="${conn.toPort}"]`);
if (!fromPort || !toPort) return;
const fromRect = fromPort.getBoundingClientRect();
const toRect = toPort.getBoundingClientRect();
const canvasRect = document.getElementById('canvas-inner').getBoundingClientRect();
const x1 = (fromRect.left + fromRect.width / 2 - canvasRect.left) / state.zoom;
const y1 = (fromRect.top + fromRect.height / 2 - canvasRect.top) / state.zoom;
const x2 = (toRect.left + toRect.width / 2 - canvasRect.left) / state.zoom;
const y2 = (toRect.top + toRect.height / 2 - canvasRect.top) / state.zoom;
const midX = (x1 + x2) / 2;
const d = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
paths += `< path class = "connection" d = "${d}" data-index = "${index}" / > `;
});
svg.innerHTML = `
< defs >
< marker id = "arrowhead" markerWidth = "10" markerHeight = "7" refX = "9" refY = "3.5" orient = "auto" >
< polygon points = "0 0, 10 3.5, 0 7" fill = "var(--primary)" / >
< / marker >
< / defs >
${paths}
`;
}
// Properties Panel
function updatePropertiesPanel() {
const content = document.getElementById('properties-content');
const empty = document.getElementById('properties-empty');
if (!state.selectedNode) {
content.style.display = 'none';
empty.style.display = 'block';
return;
}
const node = state.nodes.get(state.selectedNode);
if (!node) return;
const template = nodeTemplates[node.type];
empty.style.display = 'none';
content.style.display = 'block';
let html = `
< div class = "property-group" >
< div class = "property-group-title" > Node Info< / div >
< div class = "property-field" >
< label class = "property-label" > Type< / label >
< input type = "text" class = "property-input" value = "${node.type}" readonly >
< / div >
< div class = "property-field" >
< label class = "property-label" > ID< / label >
< input type = "text" class = "property-input" value = "${node.id}" readonly >
< / div >
< / div >
< div class = "property-group" >
< div class = "property-group-title" > Properties< / div >
`;
template.fields.forEach(field => {
const value = node.fields[field.name] || '';
if (field.type === 'textarea') {
html += `
< div class = "property-field" >
< label class = "property-label" > ${field.label}< / label >
< textarea class = "property-textarea" data-node = "${node.id}" data-field = "${field.name}" > ${value}< / textarea >
< / div >
`;
} else {
html += `
< div class = "property-field" >
< label class = "property-label" > ${field.label}< / label >
< input type = "text" class = "property-input" data-node = "${node.id}" data-field = "${field.name}" value = "${value}" >
< / div >
`;
}
});
html += '< / div > ';
content.innerHTML = html;
// Add change handlers
content.querySelectorAll('.property-input:not([readonly]), .property-textarea').forEach(input => {
input.addEventListener('change', (e) => {
const n = state.nodes.get(e.target.dataset.node);
if (n) {
n.fields[e.target.dataset.field] = e.target.value;
// Update node view
const nodeInput = document.querySelector(`#${n.id} [data-field="${e.target.dataset.field}"]`);
if (nodeInput) nodeInput.value = e.target.value;
saveToHistory();
}
});
});
}
// History (Undo/Redo)
function saveToHistory() {
const snapshot = {
nodes: Array.from(state.nodes.entries()).map(([id, node]) => ({...node})),
connections: [...state.connections]
};
state.history = state.history.slice(0, state.historyIndex + 1);
state.history.push(snapshot);
state.historyIndex = state.history.length - 1;
}
function undo() {
if (state.historyIndex > 0) {
state.historyIndex--;
restoreSnapshot(state.history[state.historyIndex]);
}
}
function redo() {
if (state.historyIndex < state.history.length - 1 ) {
state.historyIndex++;
restoreSnapshot(state.history[state.historyIndex]);
}
}
function restoreSnapshot(snapshot) {
// Clear canvas
document.getElementById('canvas-inner').innerHTML = '';
state.nodes.clear();
state.connections = [];
// Restore nodes
snapshot.nodes.forEach(node => {
state.nodes.set(node.id, {...node});
renderNode(node);
});
// Restore connections
state.connections = [...snapshot.connections];
updateConnections();
updateStatusBar();
}
// Keyboard Shortcuts
function initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch (e.key) {
case 's':
e.preventDefault();
saveDesign();
break;
case 'o':
e.preventDefault();
showModal('open-modal');
break;
case 'z':
e.preventDefault();
if (e.shiftKey) redo();
else undo();
break;
case 'y':
e.preventDefault();
redo();
break;
case 'c':
if (state.selectedNode) {
e.preventDefault();
state.clipboard = {...state.nodes.get(state.selectedNode)};
}
break;
case 'v':
if (state.clipboard) {
e.preventDefault();
const newNode = {...state.clipboard};
newNode.id = 'node-' + state.nextNodeId++;
newNode.x += 40;
newNode.y += 40;
state.nodes.set(newNode.id, newNode);
renderNode(newNode);
selectNode(newNode.id);
saveToHistory();
}
break;
}
}
if (e.key === 'Delete' & & state.selectedNode) {
deleteSelectedNode();
}
if (e.key === 'Escape') {
deselectAll();
hideContextMenu();
}
});
}
function deleteSelectedNode() {
if (!state.selectedNode) return;
const nodeEl = document.getElementById(state.selectedNode);
if (nodeEl) nodeEl.remove();
// Remove connections
state.connections = state.connections.filter(
conn => conn.from !== state.selectedNode & & conn.to !== state.selectedNode
);
state.nodes.delete(state.selectedNode);
state.selectedNode = null;
updateConnections();
updatePropertiesPanel();
updateStatusBar();
saveToHistory();
}
// Context Menu
function initContextMenu() {
const canvas = document.getElementById('canvas');
const contextMenu = document.getElementById('context-menu');
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
const nodeEl = e.target.closest('.node');
if (nodeEl) {
selectNode(nodeEl.id);
}
contextMenu.style.left = e.clientX + 'px';
contextMenu.style.top = e.clientY + 'px';
contextMenu.classList.add('visible');
});
document.addEventListener('click', () => {
hideContextMenu();
});
}
function hideContextMenu() {
document.getElementById('context-menu').classList.remove('visible');
}
// Context Menu Actions
function duplicateNode() {
if (!state.selectedNode) return;
const node = state.nodes.get(state.selectedNode);
if (!node) return;
const newNode = {...node, fields: {...node.fields}};
newNode.id = 'node-' + state.nextNodeId++;
newNode.x += 40;
newNode.y += 40;
state.nodes.set(newNode.id, newNode);
renderNode(newNode);
selectNode(newNode.id);
saveToHistory();
hideContextMenu();
}
// Status Bar
function updateStatusBar() {
document.getElementById('node-count').textContent = state.nodes.size + ' nodes';
document.getElementById('connection-count').textContent = state.connections.length + ' connections';
}
// Zoom Controls
function zoomIn() {
state.zoom = Math.min(state.zoom + 0.1, 2);
updateCanvasTransform();
updateZoomDisplay();
}
function zoomOut() {
state.zoom = Math.max(state.zoom - 0.1, 0.25);
updateCanvasTransform();
updateZoomDisplay();
}
// Modal Management
function showModal(id) {
document.getElementById(id).classList.add('visible');
if (id === 'open-modal') {
htmx.trigger('#file-list-content', 'load');
}
}
function hideModal(id) {
document.getElementById(id).classList.remove('visible');
}
// Save Design
function saveDesign() {
const nodesData = Array.from(state.nodes.values());
document.getElementById('nodes-data').value = JSON.stringify(nodesData);
document.getElementById('connections-data').value = JSON.stringify(state.connections);
2026-01-06 22:57:00 -03:00
if (state.driveSource) {
saveToDrive();
} else {
2026-01-10 10:54:05 -03:00
htmx.ajax('POST', '/api/designer/save', {
2026-01-06 22:57:00 -03:00
source: document.getElementById('designer-data'),
target: '#status-message'
});
}
}
// Save to drive (MinIO) when file was loaded from drive
async function saveToDrive() {
const basCode = generateBasCode();
const { bucket, path } = state.driveSource;
try {
const response = await fetch('/api/files/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bucket, path, content: basCode })
});
if (response.ok) {
const statusEl = document.querySelector('.status-item span');
if (statusEl) {
statusEl.textContent = `Saved: ${path.split('/').pop()}`;
}
} else {
const err = await response.json();
alert(`Save failed: ${err.error || 'Unknown error'}`);
}
} catch (e) {
alert(`Save failed: ${e.message}`);
}
}
// Generate BASIC code from nodes
function generateBasCode() {
let basCode = "' Generated by General Bots Designer\n";
basCode += "' " + new Date().toISOString() + "\n\n";
const sortedNodes = Array.from(state.nodes.values()).sort((a, b) => {
if (Math.abs(a.y - b.y) < 30 ) return a . x - b . x ;
return a.y - b.y;
2025-12-03 18:42:22 -03:00
});
2026-01-06 22:57:00 -03:00
sortedNodes.forEach(node => {
switch (node.type) {
case 'TALK':
basCode += `TALK "${node.fields.message || ''}"\n`;
break;
case 'HEAR':
basCode += `HEAR ${node.fields.variable || 'input'} AS ${node.fields.type || 'string'}\n`;
break;
case 'SET':
basCode += `SET ${node.fields.variable || 'x'} = ${node.fields.expression || '0'}\n`;
break;
case 'IF':
basCode += `IF ${node.fields.condition || 'true'} THEN\n`;
break;
case 'FOR':
basCode += `FOR EACH ${node.fields.variable || 'item'} IN ${node.fields.collection || 'items'}\n`;
break;
case 'CALL':
basCode += `CALL ${node.fields.procedure || 'sub'}(${node.fields.arguments || ''})\n`;
break;
case 'SEND MAIL':
basCode += `SEND MAIL TO "${node.fields.to || ''}" SUBJECT "${node.fields.subject || ''}" BODY "${node.fields.body || ''}"\n`;
break;
case 'GET':
basCode += `GET ${node.fields.url || 'url'} TO ${node.fields.variable || 'result'}\n`;
break;
case 'POST':
basCode += `POST ${node.fields.url || 'url'} WITH ${node.fields.body || '{}'} TO ${node.fields.variable || 'result'}\n`;
break;
case 'SAVE':
basCode += `SAVE ${node.fields.data || 'data'} TO "${node.fields.filename || 'file.txt'}"\n`;
break;
case 'WAIT':
basCode += `WAIT ${node.fields.duration || '1000'}\n`;
break;
case 'SET BOT MEMORY':
basCode += `SET BOT MEMORY "${node.fields.key || 'key'}", ${node.fields.value || '""'}\n`;
break;
case 'GET BOT MEMORY':
basCode += `GET BOT MEMORY "${node.fields.key || 'key'}" TO ${node.fields.variable || 'value'}\n`;
break;
case 'SET USER MEMORY':
basCode += `SET USER MEMORY "${node.fields.key || 'key'}", ${node.fields.value || '""'}\n`;
break;
case 'GET USER MEMORY':
basCode += `GET USER MEMORY "${node.fields.key || 'key'}" TO ${node.fields.variable || 'value'}\n`;
break;
case 'SWITCH':
basCode += `SWITCH ${node.fields.expression || 'value'}\n`;
break;
}
});
return basCode;
2025-12-03 18:42:22 -03:00
}
// Export to .bas
function exportToBas() {
let basCode = "' Generated by General Bots Designer\n";
basCode += "' " + new Date().toISOString() + "\n\n";
// Sort nodes by position (top to bottom, left to right)
const sortedNodes = Array.from(state.nodes.values()).sort((a, b) => {
if (Math.abs(a.y - b.y) < 30 ) return a . x - b . x ;
return a.y - b.y;
});
sortedNodes.forEach(node => {
const template = nodeTemplates[node.type];
switch (node.type) {
case 'TALK':
basCode += `TALK "${node.fields.message}"\n`;
break;
case 'HEAR':
basCode += `HEAR ${node.fields.variable} AS ${node.fields.type}\n`;
break;
case 'SET':
basCode += `SET ${node.fields.variable} = ${node.fields.expression}\n`;
break;
case 'IF':
basCode += `IF ${node.fields.condition} THEN\n`;
break;
case 'FOR':
basCode += `FOR EACH ${node.fields.variable} IN ${node.fields.collection}\n`;
break;
case 'CALL':
basCode += `CALL ${node.fields.procedure}(${node.fields.arguments})\n`;
break;
case 'SEND MAIL':
basCode += `SEND MAIL TO "${node.fields.to}" SUBJECT "${node.fields.subject}" BODY "${node.fields.body}"\n`;
break;
case 'GET':
basCode += `GET ${node.fields.url} TO ${node.fields.variable}\n`;
break;
case 'POST':
basCode += `POST ${node.fields.url} WITH ${node.fields.body} TO ${node.fields.variable}\n`;
break;
case 'SAVE':
basCode += `SAVE ${node.fields.data} TO "${node.fields.filename}"\n`;
break;
case 'WAIT':
basCode += `WAIT ${node.fields.duration}\n`;
break;
case 'SET BOT MEMORY':
basCode += `SET BOT MEMORY "${node.fields.key}", ${node.fields.value}\n`;
break;
case 'GET BOT MEMORY':
basCode += `GET BOT MEMORY "${node.fields.key}" AS ${node.fields.variable}\n`;
break;
case 'SET USER MEMORY':
basCode += `SET USER MEMORY "${node.fields.key}", ${node.fields.value}\n`;
break;
case 'GET USER MEMORY':
basCode += `GET USER MEMORY "${node.fields.key}" AS ${node.fields.variable}\n`;
break;
}
});
// Download as file
const blob = new Blob([basCode], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (document.getElementById('current-filename').value || 'dialog') + '.bas';
a.click();
URL.revokeObjectURL(url);
}
// New Design
function newDesign() {
if (state.nodes.size > 0) {
if (!confirm('Clear current design? Unsaved changes will be lost.')) return;
}
document.getElementById('canvas-inner').innerHTML = '';
state.nodes.clear();
state.connections = [];
state.selectedNode = null;
state.history = [];
state.historyIndex = -1;
state.nextNodeId = 1;
document.getElementById('current-filename').value = '';
document.getElementById('file-name').textContent = 'Untitled';
updateConnections();
updatePropertiesPanel();
updateStatusBar();
}
// File selection in open modal
document.addEventListener('click', (e) => {
const fileItem = e.target.closest('.file-item');
if (fileItem) {
document.querySelectorAll('.file-item').forEach(f => f.classList.remove('selected'));
fileItem.classList.add('selected');
document.getElementById('selected-file').value = fileItem.dataset.path;
}
});
2025-12-28 11:50:52 -03:00
function showMagicPanel() {
const panel = document.getElementById('magic-panel');
panel.classList.add('visible');
analyzeMagicSuggestions();
}
function hideMagicPanel() {
document.getElementById('magic-panel').classList.remove('visible');
}
async function analyzeMagicSuggestions() {
const content = document.getElementById('magic-content');
content.innerHTML = '< div class = "magic-loading" > < div class = "spinner" > < / div > < p > Analyzing your dialog...< / p > < / div > ';
const nodes = Array.from(state.nodes.values());
const dialogData = {
nodes: nodes.map(n => ({ type: n.type, fields: n.fields })),
connections: state.connections.length,
filename: document.getElementById('current-filename').value || 'untitled'
};
try {
2026-01-10 10:54:05 -03:00
const response = await fetch('/api/designer/magic', {
2025-12-28 11:50:52 -03:00
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dialogData)
});
if (response.ok) {
const suggestions = await response.json();
renderMagicSuggestions(suggestions);
} else {
renderFallbackSuggestions(dialogData);
}
} catch (e) {
renderFallbackSuggestions(dialogData);
}
}
function renderFallbackSuggestions(dialogData) {
const suggestions = [];
const nodes = dialogData.nodes;
if (!nodes.some(n => n.type === 'HEAR')) {
suggestions.push({
type: 'ux',
title: 'Add User Input',
description: 'Your dialog has no HEAR nodes. Consider adding user input to make it interactive.'
});
}
if (nodes.filter(n => n.type === 'TALK').length > 5) {
suggestions.push({
type: 'ux',
title: 'Break Up Long Responses',
description: 'You have many TALK nodes. Consider grouping related messages or using a menu.'
});
}
if (!nodes.some(n => n.type === 'IF' || n.type === 'SWITCH')) {
suggestions.push({
type: 'feature',
title: 'Add Decision Logic',
description: 'Add IF or SWITCH nodes to handle different user responses dynamically.'
});
}
if (dialogData.connections < nodes.length - 1 & & nodes . length > 1) {
suggestions.push({
type: 'perf',
title: 'Check Connections',
description: 'Some nodes may not be connected. Ensure all nodes flow properly.'
});
}
suggestions.push({
type: 'a11y',
title: 'Use Clear Language',
description: 'Keep messages short and clear. Avoid jargon for better accessibility.'
});
renderMagicSuggestions(suggestions);
}
function renderMagicSuggestions(suggestions) {
const content = document.getElementById('magic-content');
if (!suggestions || suggestions.length === 0) {
2026-01-06 22:57:00 -03:00
content.innerHTML = '< p style = "text-align:center;color:var(--text-secondary, #94a3b8);padding:40px;" > Your dialog looks great! No suggestions at this time.< / p > ';
2025-12-28 11:50:52 -03:00
return;
}
const icons = {
ux: '< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "14" height = "14" > < path d = "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" / > < / svg > ',
perf: '< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "14" height = "14" > < polygon points = "13 2 3 14 12 14 11 22 21 10 12 10 13 2" / > < / svg > ',
a11y: '< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "14" height = "14" > < circle cx = "12" cy = "12" r = "10" / > < path d = "M12 16v-4" / > < path d = "M12 8h.01" / > < / svg > ',
feature: '< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" width = "14" height = "14" > < path d = "M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" / > < / svg > '
};
content.innerHTML = suggestions.map(s => `
< div class = "magic-suggestion" >
< div class = "magic-suggestion-header" >
< div class = "magic-suggestion-icon ${s.type}" > ${icons[s.type] || icons.feature}< / div >
< span class = "magic-suggestion-title" > ${s.title}< / span >
< / div >
< p class = "magic-suggestion-desc" > ${s.description}< / p >
< / div >
`).join('');
}
document.addEventListener('keydown', (e) => {
if (e.ctrlKey & & e.key === 'm') {
e.preventDefault();
showMagicPanel();
}
});
2025-12-03 18:42:22 -03:00
< / script >