Revert "chore(ui): Nuke original web UI, in favor of React"

This reverts commit 86cb9f1282.
This commit is contained in:
Richard Palethorpe
2025-04-01 15:49:41 +01:00
parent 5023bc77f4
commit 99e0011920
24 changed files with 5246 additions and 8 deletions

714
webui/public/css/styles.css Normal file
View File

@@ -0,0 +1,714 @@
:root {
--primary: #00ff95;
--secondary: #ff00b1;
--tertiary: #5e00ff;
--dark-bg: #111111;
--darker-bg: #0a0a0a;
--medium-bg: #222222;
--light-bg: #333333;
--neon-glow: 0 0 8px rgba(0, 255, 149, 0.7);
--pink-glow: 0 0 8px rgba(255, 0, 177, 0.7);
--purple-glow: 0 0 8px rgba(94, 0, 255, 0.7);
}
/* Glitch effect animation */
@keyframes glitch {
0% { transform: translate(0); }
20% { transform: translate(-2px, 2px); }
40% { transform: translate(-2px, -2px); }
60% { transform: translate(2px, 2px); }
80% { transform: translate(2px, -2px); }
100% { transform: translate(0); }
}
/* Neon pulse animation */
@keyframes neonPulse {
0% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
50% { text-shadow: 0 0 15px var(--primary), 0 0 25px var(--primary); }
100% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
}
/* Scanning line effect */
@keyframes scanline {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
body {
font-family: 'Outfit', sans-serif;
background-color: var(--dark-bg);
color: #ffffff;
padding: 20px;
position: relative;
overflow-x: hidden;
background-image:
radial-gradient(circle at 10% 20%, rgba(0, 255, 149, 0.05) 0%, transparent 20%),
radial-gradient(circle at 90% 80%, rgba(255, 0, 177, 0.05) 0%, transparent 20%),
radial-gradient(circle at 50% 50%, rgba(94, 0, 255, 0.05) 0%, transparent 30%),
linear-gradient(180deg, var(--darker-bg) 0%, var(--dark-bg) 100%);
background-attachment: fixed;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
transparent,
transparent 2px,
rgba(0, 0, 0, 0.1) 2px,
rgba(0, 0, 0, 0.1) 4px
);
pointer-events: none;
z-index: 1000;
opacity: 0.3;
}
body::after {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
opacity: 0.7;
z-index: 1001;
animation: scanline 6s linear infinite;
pointer-events: none;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 700;
}
h1 {
font-family: 'Permanent Marker', cursive;
color: var(--primary);
text-shadow: var(--neon-glow);
margin-bottom: 1rem;
position: relative;
animation: neonPulse 2s infinite;
}
h1:hover {
animation: glitch 0.3s infinite;
}
h2 {
font-size: 1.5rem;
color: var(--secondary);
text-shadow: var(--pink-glow);
margin-bottom: 0.5rem;
}
.section-box {
background-color: rgba(17, 17, 17, 0.85);
border: 1px solid var(--primary);
padding: 25px;
margin-bottom: 20px;
border-radius: 6px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px var(--primary), inset 0 0 20px rgba(0, 0, 0, 0.3);
position: relative;
overflow: hidden;
}
.section-box::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary), var(--primary));
background-size: 200% 100%;
animation: gradientMove 3s linear infinite;
}
@keyframes gradientMove {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
input, button, textarea, select {
width: 100%;
padding: 12px;
margin-top: 8px;
border-radius: 4px;
border: 1px solid var(--medium-bg);
background-color: var(--light-bg);
color: white;
transition: all 0.3s ease;
}
input[type="text"], input[type="file"], textarea {
background-color: var(--light-bg);
border-left: 3px solid var(--primary);
color: white;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--primary);
box-shadow: var(--neon-glow);
}
button {
background: linear-gradient(135deg, var(--tertiary), var(--secondary));
color: white;
cursor: pointer;
border: none;
position: relative;
overflow: hidden;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
}
button::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: all 0.5s;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.3), 0 0 10px rgba(94, 0, 255, 0.5);
}
button:hover::before {
left: 100%;
}
textarea {
height: 200px;
resize: vertical;
}
/* Select styling */
select {
appearance: none;
background-color: var(--light-bg);
border-left: 3px solid var(--tertiary);
color: white;
padding: 12px;
border-radius: 4px;
background-image: url('data:image/svg+xml;utf8,<svg fill="%23ffffff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>');
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px;
cursor: pointer;
}
select:hover {
border-color: var(--secondary);
box-shadow: 0 0 0 1px var(--secondary);
}
select:focus {
border-color: var(--tertiary);
box-shadow: var(--purple-glow);
}
select {
overflow-y: auto;
}
option {
background-color: var(--medium-bg);
color: white;
padding: 8px 10px;
}
/* Custom Scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--medium-bg);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(var(--primary), var(--secondary));
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--tertiary);
}
/* Checkbox styling */
.checkbox-custom {
position: relative;
display: inline-block;
width: 22px;
height: 22px;
margin: 5px;
cursor: pointer;
vertical-align: middle;
}
.checkbox-custom input {
opacity: 0;
width: 0;
height: 0;
}
.checkbox-custom .checkmark {
position: absolute;
top: 0;
left: 0;
height: 22px;
width: 22px;
background-color: var(--light-bg);
border-radius: 4px;
border: 1px solid var(--medium-bg);
transition: all 0.3s ease;
}
.checkbox-custom:hover .checkmark {
border-color: var(--primary);
box-shadow: var(--neon-glow);
}
.checkbox-custom input:checked ~ .checkmark {
background: linear-gradient(135deg, var(--primary), var(--tertiary));
border-color: transparent;
}
.checkbox-custom .checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-custom input:checked ~ .checkmark:after {
display: block;
}
.checkbox-custom .checkmark:after {
left: 8px;
top: 4px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* Card styling */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card-link {
text-decoration: none;
display: block;
}
.card {
background: linear-gradient(145deg, rgba(34, 34, 34, 0.9), rgba(17, 17, 17, 0.9));
border: 1px solid rgba(94, 0, 255, 0.2);
border-radius: 8px;
padding: 25px;
margin: 25px auto;
text-align: left;
width: 90%;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary));
transform: scaleX(0);
transform-origin: left;
transition: transform 0.4s ease-out;
}
.card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4), 0 0 15px rgba(94, 0, 255, 0.3);
}
.card:hover::before {
transform: scaleX(1);
}
.card h2 {
font-family: 'Outfit', sans-serif;
font-size: 1.5em;
font-weight: 600;
color: var(--primary);
margin-bottom: 0.8em;
position: relative;
display: inline-block;
}
.card a {
color: var(--secondary);
transition: color 0.3s;
text-decoration: none;
position: relative;
}
.card a:hover {
color: var(--primary);
}
.card a::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 1px;
background: var(--primary);
transform: scaleX(0);
transform-origin: right;
transition: transform 0.3s ease;
}
.card a:hover::after {
transform: scaleX(1);
transform-origin: left;
}
.card p {
color: #cccccc;
font-size: 1em;
line-height: 1.6;
}
/* Button container */
.button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-bottom: 12px;
}
/* Alert and Toast styling */
.alert {
padding: 12px 15px;
border-radius: 4px;
margin: 15px 0;
display: none;
position: relative;
border-left: 4px solid;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.alert-success {
background-color: rgba(0, 255, 149, 0.1);
border-color: var(--primary);
color: var(--primary);
}
.alert-error {
background-color: rgba(255, 0, 177, 0.1);
border-color: var(--secondary);
color: var(--secondary);
}
.toast {
position: fixed;
top: 30px;
right: 30px;
max-width: 350px;
padding: 15px 20px;
border-radius: 6px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5);
z-index: 2000;
opacity: 0;
transform: translateX(30px);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
align-items: center;
}
.toast::before {
content: "";
width: 20px;
height: 20px;
margin-right: 15px;
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
.toast-success {
background: linear-gradient(135deg, rgba(0, 255, 149, 0.9), rgba(0, 255, 149, 0.7));
color: #111111;
border-left: 4px solid var(--primary);
}
.toast-success::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23111111'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
}
.toast-error {
background: linear-gradient(135deg, rgba(255, 0, 177, 0.9), rgba(255, 0, 177, 0.7));
color: #ffffff;
border-left: 4px solid var(--secondary);
}
.toast-error::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'/%3E%3C/svg%3E");
}
.toast-visible {
opacity: 1;
transform: translateX(0);
}
/* Action buttons */
.action-btn {
background: var(--medium-bg);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 8px;
}
.action-btn i {
font-size: 1rem;
}
.action-btn:hover {
transform: translateY(-2px);
}
.start-btn {
background: linear-gradient(135deg, var(--primary), rgba(0, 255, 149, 0.7));
color: #111111;
border: none;
}
.start-btn:hover {
box-shadow: 0 0 15px rgba(0, 255, 149, 0.5);
background: var(--primary);
}
.pause-btn {
background: linear-gradient(135deg, var(--tertiary), rgba(94, 0, 255, 0.7));
color: white;
border: none;
}
.pause-btn:hover {
box-shadow: 0 0 15px rgba(94, 0, 255, 0.5);
background: var(--tertiary);
}
/* Badge styling */
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-primary {
background-color: var(--primary);
color: #111111;
}
.badge-secondary {
background-color: var(--secondary);
color: white;
}
.badge-tertiary {
background-color: var(--tertiary);
color: white;
}
/* Data display tables */
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 20px 0;
border-radius: 6px;
overflow: hidden;
}
.data-table th, .data-table td {
text-align: left;
padding: 12px 15px;
border-bottom: 1px solid var(--medium-bg);
}
.data-table th {
background-color: rgba(94, 0, 255, 0.2);
color: var(--tertiary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.85rem;
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:nth-child(odd) td {
background-color: rgba(17, 17, 17, 0.6);
}
.data-table tr:nth-child(even) td {
background-color: rgba(34, 34, 34, 0.6);
}
.data-table tr:hover td {
background-color: rgba(94, 0, 255, 0.1);
}
/* Terminal-style code display */
.code-terminal {
background-color: #0a0a0a;
border-radius: 6px;
padding: 15px;
font-family: 'Courier New', monospace;
color: #00ff95;
margin: 20px 0;
position: relative;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4);
overflow: hidden;
}
.code-terminal::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 25px;
background: #222;
display: flex;
align-items: center;
padding: 0 10px;
}
.code-terminal::after {
content: "• • •";
position: absolute;
top: 0;
left: 12px;
height: 25px;
display: flex;
align-items: center;
color: #666;
font-size: 20px;
letter-spacing: -2px;
}
.code-terminal pre {
margin-top: 25px;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
.code-terminal .prompt {
color: var(--secondary);
user-select: none;
}
/* User info badge */
.user-info {
display: flex;
align-items: center;
background: linear-gradient(135deg, rgba(17, 17, 17, 0.8), rgba(34, 34, 34, 0.8));
border: 1px solid var(--tertiary);
border-radius: 30px;
padding: 6px 15px;
margin: 10px 0;
font-size: 0.9rem;
box-shadow: var(--purple-glow);
}
.user-info::before {
content: "";
width: 10px;
height: 10px;
background-color: var(--primary);
border-radius: 50%;
margin-right: 10px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(0, 255, 149, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(0, 255, 149, 0); }
100% { box-shadow: 0 0 0 0 rgba(0, 255, 149, 0); }
}
.timestamp {
margin-left: auto;
font-family: 'Courier New', monospace;
color: var(--secondary);
}
/* Responsive design adjustments */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.card {
width: 100%;
padding: 15px;
}
.section-box {
padding: 15px;
}
.button-container {
flex-direction: column;
}
.toast {
top: 10px;
right: 10px;
left: 10px;
max-width: none;
}
}

254
webui/public/css/wizard.css Normal file
View File

@@ -0,0 +1,254 @@
/* Agent Form Wizard Styles */
.agent-form-container {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
}
/* Wizard Sidebar */
.wizard-sidebar {
width: 250px;
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem 0;
flex-shrink: 0;
}
.wizard-nav {
list-style: none;
padding: 0;
margin: 0;
}
.wizard-nav-item {
padding: 12px 20px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 4px solid transparent;
display: flex;
align-items: center;
}
.wizard-nav-item i {
margin-right: 10px;
width: 20px;
text-align: center;
}
.wizard-nav-item:hover {
background: rgba(var(--primary-rgb), 0.1);
}
.wizard-nav-item.active {
background: rgba(var(--primary-rgb), 0.15);
border-left-color: var(--primary);
color: var(--primary);
font-weight: 600;
}
/* Form Content Area */
.form-content-area {
flex: 1;
padding: 1.5rem;
background: var(--surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 1.5rem;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(var(--border-rgb), 0.5);
color: var(--text);
}
.form-section {
display: none;
}
.form-section.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Improved input styles */
.mb-4 {
margin-bottom: 1.5rem;
}
.form-section label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text);
}
.form-section input[type="text"],
.form-section input[type="number"],
.form-section textarea,
.form-section select {
width: 100%;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid rgba(var(--border-rgb), 0.8);
background-color: var(--input-bg);
color: var(--text);
font-size: 16px;
transition: border-color 0.2s ease;
}
.form-section textarea {
min-height: 120px;
resize: vertical;
}
.form-section input[type="text"]:focus,
.form-section input[type="number"]:focus,
.form-section textarea:focus,
.form-section select:focus {
border-color: var(--primary);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2);
}
/* Button Styles */
.button-container {
margin: 1.5rem 0;
}
.action-btn {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
padding: 10px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.action-btn i {
margin-right: 6px;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.action-btn:active {
transform: translateY(0);
}
/* Navigation controls - improved layout */
.wizard-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(var(--border-rgb), 0.5);
}
.wizard-controls-left {
flex: 1;
text-align: left;
}
.wizard-controls-center {
flex: 2;
text-align: center;
}
.wizard-controls-right {
flex: 1;
text-align: right;
}
.nav-btn {
background: var(--surface);
color: var(--text);
border: 1px solid rgba(var(--border-rgb), 0.8);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
}
.nav-btn i {
margin-right: 6px;
}
.nav-btn:last-child i {
margin-right: 0;
margin-left: 6px;
}
.nav-btn:hover {
background: rgba(var(--primary-rgb), 0.1);
}
.progress-indicator {
display: inline-block;
font-size: 0.9rem;
color: var(--text-muted);
font-weight: 500;
margin-top: 0.5rem;
}
.progress-dots {
display: flex;
justify-content: center;
margin-bottom: 8px;
gap: 6px;
}
.progress-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(var(--border-rgb), 0.4);
transition: all 0.2s ease;
}
.progress-dot.active {
background-color: var(--primary);
transform: scale(1.2);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.wizard-controls {
flex-direction: column;
gap: 1rem;
}
.wizard-controls-left,
.wizard-controls-center,
.wizard-controls-right {
width: 100%;
text-align: center;
}
.progress-dots {
margin: 12px 0;
}
}

BIN
webui/public/dash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
webui/public/dash2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -0,0 +1,564 @@
// Common utility functions for agent forms
const AgentFormUtils = {
// Add dynamic component based on template
addDynamicComponent: function(sectionId, templateFunction, dataItems) {
const section = document.getElementById(sectionId);
const newIndex = section.getElementsByClassName(dataItems.className).length;
// Generate HTML from template function
const newHtml = templateFunction(newIndex, dataItems);
// Add to DOM
section.insertAdjacentHTML('beforeend', newHtml);
},
// Process form data into JSON structure
processFormData: function(formData) {
const jsonData = {};
// Process basic form fields
for (const [key, value] of formData.entries()) {
// Skip the array fields as they'll be processed separately
if (!key.includes('[') && !key.includes('].')) {
// Handle checkboxes
if (value === 'on') {
jsonData[key] = true;
}
// Handle numeric fields - specifically kb_results
else if (key === 'kb_results') {
// Convert to integer or default to 3 if empty
jsonData[key] = value ? parseInt(value, 10) : 3;
// Check if the parse was successful
if (isNaN(jsonData[key])) {
showToast('Knowledge Base Results must be a number', 'error');
return null; // Indicate validation error
}
}
// Handle other numeric fields if needed
else if (key === 'periodic_runs' && value) {
// Try to parse as number if it looks like one
const numValue = parseInt(value, 10);
if (!isNaN(numValue) && String(numValue) === value) {
jsonData[key] = numValue;
} else {
jsonData[key] = value;
}
}
else {
jsonData[key] = value;
}
}
}
return jsonData;
},
// Process connectors from form
processConnectors: function(button) {
const connectors = [];
const connectorElements = document.querySelectorAll('.connector');
for (let i = 0; i < connectorElements.length; i++) {
const typeSelect = document.getElementById(`connectorType${i}`);
if (!typeSelect) {
showToast(`Error: Could not find connector type select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const type = typeSelect.value;
if (!type) {
showToast(`Please select a connector type for connector ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
// Get all config fields for this connector
const connector = {
type: type,
config: {}
};
// Find all config inputs for this connector
const configInputs = document.querySelectorAll(`[name^="connectors[${i}][config]"]`);
// Check if we have a JSON textarea (fallback template)
const jsonTextarea = document.getElementById(`connectorConfig${i}`);
if (jsonTextarea && jsonTextarea.value) {
try {
// If it's a JSON textarea, parse it and use the result
const jsonConfig = JSON.parse(jsonTextarea.value);
// Convert the parsed JSON back to a string for the backend
connector.config = JSON.stringify(jsonConfig);
} catch (e) {
// If it's not valid JSON, use it as is
connector.config = jsonTextarea.value;
}
} else {
// Process individual form fields
configInputs.forEach(input => {
// Extract the key from the name attribute
// Format: connectors[0][config][key]
const keyMatch = input.name.match(/\[config\]\[([^\]]+)\]/);
if (keyMatch && keyMatch[1]) {
const key = keyMatch[1];
// For checkboxes, set true/false based on checked state
if (input.type === 'checkbox') {
connector.config[key] = input.checked ? 'true' : 'false';
} else {
connector.config[key] = input.value;
}
}
});
// Convert the config object to a JSON string for the backend
connector.config = JSON.stringify(connector.config);
}
connectors.push(connector);
}
return connectors;
},
// Process MCP servers from form
processMCPServers: function() {
const mcpServers = [];
const mcpElements = document.querySelectorAll('.mcp_server');
for (let i = 0; i < mcpElements.length; i++) {
const urlInput = document.getElementById(`mcpURL${i}`);
const tokenInput = document.getElementById(`mcpToken${i}`);
if (urlInput && urlInput.value) {
const server = {
url: urlInput.value
};
// Add token if present
if (tokenInput && tokenInput.value) {
server.token = tokenInput.value;
}
mcpServers.push(server);
}
}
return mcpServers;
},
// Process actions from form
processActions: function(button) {
const actions = [];
const actionElements = document.querySelectorAll('.action');
for (let i = 0; i < actionElements.length; i++) {
const nameSelect = document.getElementById(`actionsName${i}`);
const configTextarea = document.getElementById(`actionsConfig${i}`);
if (!nameSelect) {
showToast(`Error: Could not find action name select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const name = nameSelect.value;
if (!name) {
showToast(`Please select an action type for action ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
let config = {};
if (configTextarea && configTextarea.value) {
try {
config = JSON.parse(configTextarea.value);
} catch (e) {
showToast(`Invalid JSON in action ${i+1} config: ${e.message}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
}
actions.push({
name: name,
config: JSON.stringify(config) // Convert to JSON string for backend
});
}
return actions;
},
// Process prompt blocks from form
processPromptBlocks: function(button) {
const promptBlocks = [];
const promptElements = document.querySelectorAll('.prompt_block');
for (let i = 0; i < promptElements.length; i++) {
const nameSelect = document.getElementById(`promptName${i}`);
const configTextarea = document.getElementById(`promptConfig${i}`);
if (!nameSelect) {
showToast(`Error: Could not find prompt block name select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const name = nameSelect.value;
if (!name) {
showToast(`Please select a prompt block type for block ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
let config = {};
if (configTextarea && configTextarea.value) {
try {
config = JSON.parse(configTextarea.value);
} catch (e) {
showToast(`Invalid JSON in prompt block ${i+1} config: ${e.message}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
}
promptBlocks.push({
name: name,
config: JSON.stringify(config) // Convert to JSON string for backend
});
}
return promptBlocks;
},
// Helper function to format config values (for edit form)
formatConfigValue: function(configElement, configValue) {
if (!configElement) return;
// If configValue is an object, stringify it
if (typeof configValue === 'object' && configValue !== null) {
try {
configElement.value = JSON.stringify(configValue, null, 2);
} catch (e) {
console.error('Error stringifying config value:', e);
configElement.value = '{}';
}
}
// If it's a string that looks like JSON, try to parse and pretty print it
else if (typeof configValue === 'string' && (configValue.startsWith('{') || configValue.startsWith('['))) {
try {
const parsed = JSON.parse(configValue);
configElement.value = JSON.stringify(parsed, null, 2);
} catch (e) {
// If it's not valid JSON, just use the string as is
configElement.value = configValue;
}
}
// Otherwise, just use the value as is
else {
configElement.value = configValue || '';
}
},
// Helper function to set select value (with fallback if option doesn't exist)
setSelectValue: function(selectElement, value) {
if (!selectElement) return;
// Check if the option exists
let optionExists = false;
for (let i = 0; i < selectElement.options.length; i++) {
if (selectElement.options[i].value === value) {
optionExists = true;
break;
}
}
// Set the value if the option exists
if (optionExists) {
selectElement.value = value;
} else if (selectElement.options.length > 0) {
// Otherwise, select the first option
selectElement.selectedIndex = 0;
}
},
// Render connector form based on type
renderConnectorForm: function(index, type, config = {}) {
const formContainer = document.getElementById(`connectorFormContainer${index}`);
if (!formContainer) return;
// Clear existing form
formContainer.innerHTML = '';
// Debug log to see what's happening
console.log(`Rendering connector form for type: ${type}`);
console.log(`Config for connector:`, config);
console.log(`Available templates:`, ConnectorTemplates ? Object.keys(ConnectorTemplates) : 'None');
// Ensure config is an object
let configObj = config;
if (typeof config === 'string') {
try {
configObj = JSON.parse(config);
} catch (e) {
console.error('Error parsing connector config string:', e);
configObj = {};
}
}
// If we have a template for this connector type in the global ConnectorTemplates object
if (ConnectorTemplates && type && ConnectorTemplates[type]) {
console.log(`Found template for ${type}`);
// Get the template result which contains HTML and setValues function
const templateResult = ConnectorTemplates[type](configObj, index);
// Set the HTML content
formContainer.innerHTML = templateResult.html;
// Call the setValues function to set input values safely
if (typeof templateResult.setValues === 'function') {
setTimeout(templateResult.setValues, 0);
}
} else {
console.log(`No template found for ${type}, using fallback`);
// Use the fallback template
if (ConnectorTemplates && ConnectorTemplates.fallback) {
const fallbackResult = ConnectorTemplates.fallback(configObj, index);
formContainer.innerHTML = fallbackResult.html;
if (typeof fallbackResult.setValues === 'function') {
setTimeout(fallbackResult.setValues, 0);
}
} else {
// Fallback to generic JSON textarea if no fallback template
formContainer.innerHTML = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Config (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
placeholder='{"key":"value"}'></textarea>
</div>
`;
// Set the value safely after DOM is created
setTimeout(function() {
const configTextarea = document.getElementById(`connectorConfig${index}`);
if (configTextarea) {
if (typeof configObj === 'object' && configObj !== null) {
configTextarea.value = JSON.stringify(configObj, null, 2);
} else if (typeof config === 'string') {
configTextarea.value = config;
}
}
}, 0);
}
}
}
};
// HTML Templates for dynamic elements
const AgentFormTemplates = {
// Connector template
connectorTemplate: function(index, data) {
return `
<div class="connector mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Connector ${index + 1}</h2>
<div class="mb-4">
<label for="connectorType${index}">Connector Type</label>
<select name="connectors[${index}][type]"
id="connectorType${index}"
class="form-control"
onchange="AgentFormUtils.renderConnectorForm(${index}, this.value)">
<option value="">Select Connector Type</option>
${data.options}
</select>
</div>
<div id="connectorFormContainer${index}">
<!-- Connector form will be dynamically inserted here -->
<div class="form-group">
<div class="placeholder-text">Select a connector type to configure</div>
</div>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.connector').remove()">
<i class="fas fa-trash"></i> Remove Connector
</button>
</div>
`;
},
// MCP Server template
mcpServerTemplate: function(index, data) {
return `
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>MCP Server ${index + 1}</h2>
<div class="mb-4">
<label for="mcpURL${index}">Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}][url]" placeholder="https://example.com">
</div>
<div class="mb-4">
<label for="mcpToken${index}">API Token (Optional)</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}][token]" placeholder="API token">
</div>
<button type="button" class="remove-btn" onclick="this.closest('.mcp_server').remove()">
<i class="fas fa-trash"></i> Remove Server
</button>
</div>
`;
},
// Action template
actionTemplate: function(index, data) {
return `
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Action ${index + 1}</h2>
<div class="mb-4">
<label for="actionsName${index}">Action Type</label>
<select name="actions[${index}][name]" id="actionsName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="actionsConfig${index}">Action Config (JSON)</label>
<textarea id="actionsConfig${index}" name="actions[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.action').remove()">
<i class="fas fa-trash"></i> Remove Action
</button>
</div>
`;
},
// Prompt Block template
promptBlockTemplate: function(index, data) {
return `
<div class="prompt_block mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Prompt Block ${index + 1}</h2>
<div class="mb-4">
<label for="promptName${index}">Prompt Block Type</label>
<select name="promptblocks[${index}][name]" id="promptName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="promptConfig${index}">Prompt Block Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.prompt_block').remove()">
<i class="fas fa-trash"></i> Remove Prompt Block
</button>
</div>
`;
}
};
// Initialize form event listeners
function initAgentFormCommon(options = {}) {
// Add connector button
const addConnectorButton = document.getElementById('addConnectorButton');
if (addConnectorButton) {
addConnectorButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.connectors) {
optionsHtml = options.connectors;
}
// Add new connector form
AgentFormUtils.addDynamicComponent('connectorsSection', AgentFormTemplates.connectorTemplate, {
className: 'connector',
options: optionsHtml
});
});
}
// Add MCP server button
const addMCPButton = document.getElementById('addMCPButton');
if (addMCPButton) {
addMCPButton.addEventListener('click', function() {
// Add new MCP server form
AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, {
className: 'mcp_server'
});
});
}
// Add action button
const actionButton = document.getElementById('action_button');
if (actionButton) {
actionButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.actions) {
optionsHtml = options.actions;
}
// Add new action form
AgentFormUtils.addDynamicComponent('action_box', AgentFormTemplates.actionTemplate, {
className: 'action',
options: optionsHtml
});
});
}
// Add prompt block button
const dynamicButton = document.getElementById('dynamic_button');
if (dynamicButton) {
dynamicButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.promptBlocks) {
optionsHtml = options.promptBlocks;
}
// Add new prompt block form
AgentFormUtils.addDynamicComponent('dynamic_box', AgentFormTemplates.promptBlockTemplate, {
className: 'prompt_block',
options: optionsHtml
});
});
}
}
// Simple toast notification function
function showToast(message, type) {
// Check if toast container exists, if not create it
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
toast.className = 'toast';
const toastMessage = document.createElement('div');
toastMessage.id = 'toast-message';
toast.appendChild(toastMessage);
document.body.appendChild(toast);
}
const toastMessage = document.getElementById('toast-message');
// Set message
toastMessage.textContent = message;
// Set type class
toast.className = 'toast';
toast.classList.add(`toast-${type}`);
// Show toast
toast.classList.add('show');
// Hide after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}

44
webui/public/js/common.js Normal file
View File

@@ -0,0 +1,44 @@
// Function to show toast notifications with enhanced animation
function showToast(message, type) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
// Set message
toastMessage.textContent = message;
// Set toast type (success/error)
toast.className = 'toast';
toast.classList.add(type === 'success' ? 'toast-success' : 'toast-error');
// Show toast with enhanced animation
setTimeout(() => {
toast.classList.add('toast-visible');
}, 100);
// Hide toast after 3 seconds with animation
setTimeout(() => {
toast.classList.remove('toast-visible');
// Clean up after animation completes
setTimeout(() => {
toast.className = 'toast';
}, 400);
}, 3000);
}
// Function to create the glitch effect on headings
document.addEventListener('DOMContentLoaded', function() {
const headings = document.querySelectorAll('h1');
headings.forEach(heading => {
heading.addEventListener('mouseover', function() {
this.style.animation = 'glitch 0.3s infinite';
});
heading.addEventListener('mouseout', function() {
this.style.animation = 'neonPulse 2s infinite';
});
});
});

View File

@@ -0,0 +1,475 @@
/**
* Connector Templates
*
* This file contains templates for all connector types supported by LocalAgent.
* Each template is a function that returns an HTML string for the connector's form.
*
* Note: We don't need to escape HTML in the value attributes because browsers
* handle these values safely when setting them via DOM properties after rendering.
*/
/**
* Connector Templates
* Each function takes a config object and returns an HTML string
*/
const ConnectorTemplates = {
/**
* Telegram Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
telegram: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="telegramToken${index}">Telegram Bot Token</label>
<input type="text"
id="telegramToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
<small class="form-text text-muted">Get this from @BotFather on Telegram</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const input = document.getElementById(`telegramToken${index}`);
if (input) input.value = config.token || '';
};
return { html, setValues };
},
/**
* Slack Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
slack: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="slackAppToken${index}">Slack App Token</label>
<input type="text"
id="slackAppToken${index}"
name="connectors[${index}][config][appToken]"
class="form-control"
placeholder="xapp-...">
<small class="form-text text-muted">App-level token starting with xapp-</small>
</div>
<div class="form-group">
<label for="slackBotToken${index}">Slack Bot Token</label>
<input type="text"
id="slackBotToken${index}"
name="connectors[${index}][config][botToken]"
class="form-control"
placeholder="xoxb-...">
<small class="form-text text-muted">Bot token starting with xoxb-</small>
</div>
<div class="form-group">
<label for="slackChannelID${index}">Slack Channel ID</label>
<input type="text"
id="slackChannelID${index}"
name="connectors[${index}][config][channelID]"
class="form-control"
placeholder="C012345678">
<small class="form-text text-muted">Channel ID where the bot will operate</small>
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="slackAlwaysReply${index}"
name="connectors[${index}][config][alwaysReply]">
<label class="form-check-label" for="slackAlwaysReply${index}">Always Reply</label>
<small class="form-text text-muted">If checked, the bot will reply to all messages in the channel</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const appTokenInput = document.getElementById(`slackAppToken${index}`);
const botTokenInput = document.getElementById(`slackBotToken${index}`);
const channelIDInput = document.getElementById(`slackChannelID${index}`);
const alwaysReplyInput = document.getElementById(`slackAlwaysReply${index}`);
if (appTokenInput) appTokenInput.value = config.appToken || '';
if (botTokenInput) botTokenInput.value = config.botToken || '';
if (channelIDInput) channelIDInput.value = config.channelID || '';
if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true';
};
return { html, setValues };
},
/**
* Discord Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
discord: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="discordToken${index}">Discord Bot Token</label>
<input type="text"
id="discordToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="Bot token from Discord Developer Portal">
</div>
<div class="form-group">
<label for="discordChannelID${index}">Default Channel ID</label>
<input type="text"
id="discordChannelID${index}"
name="connectors[${index}][config][defaultChannel]"
class="form-control"
placeholder="Channel ID where the bot will operate">
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const tokenInput = document.getElementById(`discordToken${index}`);
const channelIDInput = document.getElementById(`discordChannelID${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (channelIDInput) channelIDInput.value = config.defaultChannel || '';
};
return { html, setValues };
},
/**
* GitHub Issues Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
'github-issues': function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="githubIssuesToken${index}">GitHub Personal Access Token</label>
<input type="text"
id="githubIssuesToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="ghp_...">
<small class="form-text text-muted">Needs repo and read:org permissions</small>
</div>
<div class="form-group">
<label for="githubIssuesOwner${index}">Repository Owner</label>
<input type="text"
id="githubIssuesOwner${index}"
name="connectors[${index}][config][owner]"
class="form-control"
placeholder="username or organization">
</div>
<div class="form-group">
<label for="githubIssuesRepo${index}">Repository Name</label>
<input type="text"
id="githubIssuesRepo${index}"
name="connectors[${index}][config][repository]"
class="form-control"
placeholder="repository-name">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="githubIssuesReplyIfNoReplies${index}"
name="connectors[${index}][config][replyIfNoReplies]"
value="true">
<label class="form-check-label" for="githubIssuesReplyIfNoReplies${index}">Reply to issues with no replies</label>
<small class="form-text text-muted">If checked, the bot will reply to issues that have no replies yet</small>
</div>
<div class="form-group">
<label for="githubIssuesPollInterval${index}">Poll Interval (seconds)</label>
<input type="number"
id="githubIssuesPollInterval${index}"
name="connectors[${index}][config][pollInterval]"
class="form-control"
placeholder="60">
<small class="form-text text-muted">How often to check for new issues (in seconds)</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const tokenInput = document.getElementById(`githubIssuesToken${index}`);
const ownerInput = document.getElementById(`githubIssuesOwner${index}`);
const repoInput = document.getElementById(`githubIssuesRepo${index}`);
const replyIfNoRepliesInput = document.getElementById(`githubIssuesReplyIfNoReplies${index}`);
const pollIntervalInput = document.getElementById(`githubIssuesPollInterval${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (ownerInput) ownerInput.value = config.owner || '';
if (repoInput) repoInput.value = config.repository || '';
if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true';
if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60';
};
return { html, setValues };
},
/**
* GitHub PRs Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
'github-prs': function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="githubPRsToken${index}">GitHub Token</label>
<input type="text"
id="githubPRsToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="ghp_...">
<small class="form-text text-muted">Personal Access Token with repo permissions</small>
</div>
<div class="form-group">
<label for="githubPRsOwner${index}">Repository Owner</label>
<input type="text"
id="githubPRsOwner${index}"
name="connectors[${index}][config][owner]"
class="form-control"
placeholder="username or organization">
</div>
<div class="form-group">
<label for="githubPRsRepo${index}">Repository Name</label>
<input type="text"
id="githubPRsRepo${index}"
name="connectors[${index}][config][repository]"
class="form-control"
placeholder="repository-name">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="githubPRsReplyIfNoReplies${index}"
name="connectors[${index}][config][replyIfNoReplies]"
value="true">
<label class="form-check-label" for="githubPRsReplyIfNoReplies${index}">Reply to PRs with no replies</label>
<small class="form-text text-muted">If checked, the bot will reply to pull requests that have no replies yet</small>
</div>
<div class="form-group">
<label for="githubPRsPollInterval${index}">Poll Interval (seconds)</label>
<input type="number"
id="githubPRsPollInterval${index}"
name="connectors[${index}][config][pollInterval]"
class="form-control"
placeholder="60">
<small class="form-text text-muted">How often to check for new pull requests (in seconds)</small>
</div>
`;
// Function to set values after HTML is added to DOM to avoid XSS
const setValues = function() {
const tokenInput = document.getElementById(`githubPRsToken${index}`);
const ownerInput = document.getElementById(`githubPRsOwner${index}`);
const repoInput = document.getElementById(`githubPRsRepo${index}`);
const replyIfNoRepliesInput = document.getElementById(`githubPRsReplyIfNoReplies${index}`);
const pollIntervalInput = document.getElementById(`githubPRsPollInterval${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (ownerInput) ownerInput.value = config.owner || '';
if (repoInput) repoInput.value = config.repository || '';
if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true';
if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60';
};
return { html, setValues };
},
/**
* IRC Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
irc: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="ircServer${index}">IRC Server</label>
<input type="text"
id="ircServer${index}"
name="connectors[${index}][config][server]"
class="form-control"
placeholder="irc.libera.chat">
</div>
<div class="form-group">
<label for="ircPort${index}">Port</label>
<input type="text"
id="ircPort${index}"
name="connectors[${index}][config][port]"
class="form-control"
placeholder="6667">
</div>
<div class="form-group">
<label for="ircChannel${index}">Channel</label>
<input type="text"
id="ircChannel${index}"
name="connectors[${index}][config][channel]"
class="form-control"
placeholder="#channel">
</div>
<div class="form-group">
<label for="ircNick${index}">Nickname</label>
<input type="text"
id="ircNick${index}"
name="connectors[${index}][config][nickname]"
class="form-control"
placeholder="MyBot">
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="ircAlwaysReply${index}"
name="connectors[${index}][config][alwaysReply]"
value="true">
<label class="form-check-label" for="ircAlwaysReply${index}">Always reply to messages</label>
<small class="form-text text-muted">If checked, the bot will always reply to messages, even if they are not directed at it</small>
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const serverInput = document.getElementById(`ircServer${index}`);
const portInput = document.getElementById(`ircPort${index}`);
const channelInput = document.getElementById(`ircChannel${index}`);
const nickInput = document.getElementById(`ircNick${index}`);
const alwaysReplyInput = document.getElementById(`ircAlwaysReply${index}`);
if (serverInput) serverInput.value = config.server || '';
if (portInput) portInput.value = config.port || '6667';
if (channelInput) channelInput.value = config.channel || '';
if (nickInput) nickInput.value = config.nickname || '';
if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true';
};
return { html, setValues };
},
/**
* Twitter Connector Template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
twitter: function(config = {}, index) {
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="twitterToken${index}">Twitter API Token</label>
<input type="text"
id="twitterToken${index}"
name="connectors[${index}][config][token]"
class="form-control"
placeholder="Your Twitter API token">
</div>
<div class="form-group">
<label for="twitterBotUsername${index}">Bot Username</label>
<input type="text"
id="twitterBotUsername${index}"
name="connectors[${index}][config][botUsername]"
class="form-control"
placeholder="@YourBotUsername">
<small class="form-text text-muted">Username of your Twitter bot (with or without @)</small>
</div>
<div class="form-group form-check">
<input type="checkbox"
class="form-check-input"
id="twitterNoCharLimit${index}"
name="connectors[${index}][config][noCharacterLimit]"
value="true">
<label class="form-check-label" for="twitterNoCharLimit${index}">Disable character limit</label>
<small class="form-text text-muted">If checked, the bot will not enforce Twitter's character limit</small>
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const tokenInput = document.getElementById(`twitterToken${index}`);
const botUsernameInput = document.getElementById(`twitterBotUsername${index}`);
const noCharLimitInput = document.getElementById(`twitterNoCharLimit${index}`);
if (tokenInput) tokenInput.value = config.token || '';
if (botUsernameInput) botUsernameInput.value = config.botUsername || '';
if (noCharLimitInput) noCharLimitInput.checked = config.noCharacterLimit === 'true';
};
return { html, setValues };
},
/**
* Fallback template for any connector without a specific template
* @param {Object} config - Existing configuration values
* @param {Number} index - Connector index
* @returns {Object} HTML template and setValues function
*/
fallback: function(config = {}, index) {
// Convert config to a pretty-printed JSON string
let configStr = '{}';
try {
if (typeof config === 'string') {
// If it's already a string, try to parse it first to pretty-print
configStr = JSON.stringify(JSON.parse(config), null, 2);
} else if (typeof config === 'object' && config !== null) {
configStr = JSON.stringify(config, null, 2);
}
} catch (e) {
console.error('Error formatting config:', e);
// If it's a string but not valid JSON, just use it as is
if (typeof config === 'string') {
configStr = config;
}
}
// Return HTML without values in the template string
const html = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Configuration (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
rows="10"
placeholder='{"key":"value"}'>${escapeHTML(configStr)}</textarea>
<small class="form-text text-muted">Enter the connector configuration as a JSON object</small>
</div>
`;
// Function to set values after HTML is added to DOM
const setValues = function() {
const configInput = document.getElementById(`connectorConfig${index}`);
if (configInput) configInput.value = configStr;
};
return { html, setValues };
}
};

139
webui/public/js/wizard.js Normal file
View File

@@ -0,0 +1,139 @@
/**
* Agent Form Wizard - Navigation and UI functionality
*/
document.addEventListener('DOMContentLoaded', function() {
// Check if the wizard exists on the page
const wizardSidebar = document.querySelector('.wizard-sidebar');
if (!wizardSidebar) return;
// Get all sections and nav items
const navItems = document.querySelectorAll('.wizard-nav-item');
const sections = document.querySelectorAll('.form-section');
const prevButton = document.getElementById('prevSection');
const nextButton = document.getElementById('nextSection');
const currentStepLabelEl = document.getElementById('currentStepLabel');
const progressDotsContainer = document.getElementById('progressDots');
// Create progress dots
const totalSteps = sections.length;
// Create dots for each section
if (progressDotsContainer) {
for (let i = 0; i < totalSteps; i++) {
const dot = document.createElement('div');
dot.className = 'progress-dot';
dot.setAttribute('data-index', i);
dot.addEventListener('click', () => setActiveSection(i));
progressDotsContainer.appendChild(dot);
}
}
// Get all progress dots
const progressDots = document.querySelectorAll('.progress-dot');
// Track current active section
let currentSectionIndex = 0;
// Initialize
updateNavigation();
// Add click events to nav items
navItems.forEach((item, index) => {
item.addEventListener('click', () => {
setActiveSection(index);
});
});
// Add click events to prev/next buttons
if (prevButton) {
prevButton.addEventListener('click', () => {
if (currentSectionIndex > 0) {
setActiveSection(currentSectionIndex - 1);
}
});
}
if (nextButton) {
nextButton.addEventListener('click', () => {
if (currentSectionIndex < sections.length - 1) {
setActiveSection(currentSectionIndex + 1);
}
});
}
/**
* Set the active section and update navigation
*/
function setActiveSection(index) {
// Remove active class from all sections and nav items
sections.forEach(section => section.classList.remove('active'));
navItems.forEach(item => item.classList.remove('active'));
progressDots.forEach(dot => dot.classList.remove('active'));
// Add active class to current section, nav item, and dot
sections[index].classList.add('active');
navItems[index].classList.add('active');
if (progressDots[index]) {
progressDots[index].classList.add('active');
}
// Update current section index
currentSectionIndex = index;
// Update navigation state
updateNavigation();
// Scroll to top of section
sections[index].scrollIntoView({behavior: 'smooth', block: 'start'});
}
/**
* Update navigation buttons and progress
*/
function updateNavigation() {
// Update section label
if (currentStepLabelEl && navItems[currentSectionIndex]) {
// Extract text content without the icon
const navText = navItems[currentSectionIndex].textContent.trim();
currentStepLabelEl.textContent = navText;
}
// Update prev/next buttons
if (prevButton) {
prevButton.disabled = currentSectionIndex === 0;
prevButton.style.opacity = currentSectionIndex === 0 ? 0.5 : 1;
}
if (nextButton) {
nextButton.disabled = currentSectionIndex === sections.length - 1;
nextButton.style.opacity = currentSectionIndex === sections.length - 1 ? 0.5 : 1;
// Change text for last step
if (currentSectionIndex === sections.length - 2) {
nextButton.innerHTML = 'Finish <i class="fas fa-check"></i>';
} else {
nextButton.innerHTML = 'Next <i class="fas fa-arrow-right"></i>';
}
}
}
// Helper function to validate current section before proceeding
function validateCurrentSection() {
// Implement validation logic here based on the current section
// Return true if valid, false if not
return true;
}
// Add to initAgentFormCommon function if it exists
if (typeof window.initAgentFormCommon === 'function') {
const originalInit = window.initAgentFormCommon;
window.initAgentFormCommon = function(options) {
// Call the original initialization function
originalInit(options);
// Now initialize the wizard navigation
setActiveSection(0);
};
}
});

BIN
webui/public/logo_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB