From 35c75b61d83b8dbfea2d85ae291b71731529945e Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sun, 16 Mar 2025 22:59:48 +0100 Subject: [PATCH] Refactor views --- webui/public/css/styles.css | 714 +++++++++++++++++++++++++ webui/public/js/agent-form.js | 368 +++++++++++++ webui/public/js/common.js | 44 ++ webui/views/create.html | 447 +--------------- webui/views/partials/agent-form.html | 173 ++++++ webui/views/partials/header.html | 765 +-------------------------- webui/views/settings.html | 500 ++--------------- 7 files changed, 1356 insertions(+), 1655 deletions(-) create mode 100644 webui/public/css/styles.css create mode 100644 webui/public/js/agent-form.js create mode 100644 webui/public/js/common.js create mode 100644 webui/views/partials/agent-form.html diff --git a/webui/public/css/styles.css b/webui/public/css/styles.css new file mode 100644 index 0000000..f466cbf --- /dev/null +++ b/webui/public/css/styles.css @@ -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,'); + 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; + } +} \ No newline at end of file diff --git a/webui/public/js/agent-form.js b/webui/public/js/agent-form.js new file mode 100644 index 0000000..8f1c9b8 --- /dev/null +++ b/webui/public/js/agent-form.js @@ -0,0 +1,368 @@ +// 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 connectorsElements = document.getElementsByClassName('connector'); + + for (let i = 0; i < connectorsElements.length; i++) { + const typeField = document.getElementById(`connectorType${i}`); + const configField = document.getElementById(`connectorConfig${i}`); + + if (typeField && configField) { + try { + // Validate JSON but send as string + const configValue = configField.value.trim() || '{}'; + // Parse to validate but don't use the parsed object + JSON.parse(configValue); + + connectors.push({ + type: typeField.value, + config: configValue // Send the raw string, not parsed JSON + }); + } catch (err) { + console.error(`Error parsing connector ${i} config:`, err); + showToast(`Error in connector ${i+1} configuration: Invalid JSON`, 'error'); + + // If button is provided, restore its state + if (button) { + const originalButtonText = button.getAttribute('data-original-text'); + button.innerHTML = originalButtonText; + button.disabled = false; + } + + return null; // Indicate validation error + } + } + } + + return connectors; + }, + + // Process MCP servers from form + processMCPServers: function() { + const mcpServers = []; + const mcpServerElements = document.getElementsByClassName('mcp_server'); + + for (let i = 0; i < mcpServerElements.length; i++) { + const urlField = document.getElementById(`mcpURL${i}`); + const tokenField = document.getElementById(`mcpToken${i}`); + + if (urlField && urlField.value.trim()) { + mcpServers.push({ + url: urlField.value.trim(), + token: tokenField ? tokenField.value.trim() : '' + }); + } + } + + return mcpServers; + }, + + // Process actions from form + processActions: function(button) { + const actions = []; + const actionElements = document.getElementsByClassName('action'); + + for (let i = 0; i < actionElements.length; i++) { + const nameField = document.getElementById(`actionsName${i}`); + const configField = document.getElementById(`actionsConfig${i}`); + + if (nameField && configField) { + try { + // Validate JSON but send as string + const configValue = configField.value.trim() || '{}'; + // Parse to validate but don't use the parsed object + JSON.parse(configValue); + + actions.push({ + name: nameField.value, + config: configValue // Send the raw string, not parsed JSON + }); + } catch (err) { + console.error(`Error parsing action ${i} config:`, err); + showToast(`Error in action ${i+1} configuration: Invalid JSON`, 'error'); + + // If button is provided, restore its state + if (button) { + const originalButtonText = button.getAttribute('data-original-text'); + button.innerHTML = originalButtonText; + button.disabled = false; + } + + return null; // Indicate validation error + } + } + } + + return actions; + }, + + // Process prompt blocks from form + processPromptBlocks: function(button) { + const promptBlocks = []; + const promptBlockElements = document.getElementsByClassName('promptBlock'); + + for (let i = 0; i < promptBlockElements.length; i++) { + const nameField = document.getElementById(`promptName${i}`); + const configField = document.getElementById(`promptConfig${i}`); + + if (nameField && configField) { + try { + // Validate JSON but send as string + const configValue = configField.value.trim() || '{}'; + // Parse to validate but don't use the parsed object + JSON.parse(configValue); + + promptBlocks.push({ + name: nameField.value, + config: configValue // Send the raw string, not parsed JSON + }); + } catch (err) { + console.error(`Error parsing prompt block ${i} config:`, err); + showToast(`Error in prompt block ${i+1} configuration: Invalid JSON`, 'error'); + + // If button is provided, restore its state + if (button) { + const originalButtonText = button.getAttribute('data-original-text'); + button.innerHTML = originalButtonText; + button.disabled = false; + } + + return null; // Indicate validation error + } + } + } + + return promptBlocks; + }, + + // Helper function to format config values (for edit form) + formatConfigValue: function(configElement, configValue) { + // If it's a string (already stringified JSON), try to parse it first + if (typeof configValue === 'string') { + try { + const parsed = JSON.parse(configValue); + configElement.value = JSON.stringify(parsed, null, 2); + } catch (e) { + console.warn("Failed to parse config JSON string:", e); + configElement.value = configValue; // Keep as is if parsing fails + } + } else if (configValue !== undefined && configValue !== null) { + // Direct object, just stringify with formatting + configElement.value = JSON.stringify(configValue, null, 2); + } else { + // Default to empty object + configElement.value = '{}'; + } + }, + + // Helper function to set select value (with fallback if option doesn't exist) + setSelectValue: function(selectElement, value) { + // Check if the option exists + const optionExists = Array.from(selectElement.options).some(option => option.value === value); + + if (optionExists) { + selectElement.value = value; + } else if (value) { + // If value is provided but option doesn't exist, create a new option + const newOption = document.createElement('option'); + newOption.value = value; + newOption.text = value + ' (custom)'; + selectElement.add(newOption); + selectElement.value = value; + } + } +}; + +// HTML Templates for dynamic elements +const AgentFormTemplates = { + // Connector template + connectorTemplate: function(index, data) { + return ` +
+

Connector ${index + 1}

+
+ + +
+
+ + +
+
+ `; + }, + + // MCP Server template + mcpServerTemplate: function(index, data) { + return ` +
+

MCP Server ${index + 1}

+
+ + +
+
+ + +
+
+ `; + }, + + // Action template + actionTemplate: function(index, data) { + return ` +
+

Action ${index + 1}

+
+ + +
+
+ + +
+
+ `; + }, + + // Prompt Block template + promptBlockTemplate: function(index, data) { + return ` +
+

Prompt Block ${index + 1}

+
+ + +
+
+ + +
+
+ `; + } +}; + +// Initialize form event listeners +function initAgentFormCommon(options = {}) { + // Setup event listeners for dynamic component buttons + if (options.enableConnectors !== false) { + document.getElementById('addConnectorButton').addEventListener('click', function() { + AgentFormUtils.addDynamicComponent('connectorsSection', AgentFormTemplates.connectorTemplate, { + className: 'connector', + options: options.connectors || '' + }); + }); + } + + if (options.enableMCP !== false) { + document.getElementById('addMCPButton').addEventListener('click', function() { + AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, { + className: 'mcp_server' + }); + }); + } + + if (options.enableActions !== false) { + document.getElementById('action_button').addEventListener('click', function() { + AgentFormUtils.addDynamicComponent('action_box', AgentFormTemplates.actionTemplate, { + className: 'action', + options: options.actions || '' + }); + }); + } + + if (options.enablePromptBlocks !== false) { + document.getElementById('dynamic_button').addEventListener('click', function() { + AgentFormUtils.addDynamicComponent('dynamic_box', AgentFormTemplates.promptBlockTemplate, { + className: 'promptBlock', + options: options.promptBlocks || '' + }); + }); + } + + // Add additional CSS for checkbox labels + const style = document.createElement('style'); + style.textContent = ` + .checkbox-label { + display: flex; + align-items: center; + cursor: pointer; + margin-bottom: 10px; + } + + .checkbox-label .checkbox-custom { + margin-right: 10px; + } + + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } + } + `; + document.head.appendChild(style); +} \ No newline at end of file diff --git a/webui/public/js/common.js b/webui/public/js/common.js new file mode 100644 index 0000000..d0cf5eb --- /dev/null +++ b/webui/public/js/common.js @@ -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'; + }); + }); + + +}); \ No newline at end of file diff --git a/webui/views/create.html b/webui/views/create.html index 0a7beca..66305b2 100644 --- a/webui/views/create.html +++ b/webui/views/create.html @@ -3,6 +3,7 @@ Create New Agent {{template "views/partials/header"}} + {{template "views/partials/menu"}} @@ -11,176 +12,9 @@

Create New Agent

-
- - -
+ {{template "views/partials/agent-form" . }} -
- -
- -
- -
- -
- -
- -
- -
- -
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
@@ -209,100 +43,15 @@ const connectors = `{{ range .Connectors }}{{ end }}`; const promptBlocks = `{{ range .PromptBlocks }}{{ end }}`; - // Add connector functionality - document.getElementById('addConnectorButton').addEventListener('click', function() { - const connectorsSection = document.getElementById('connectorsSection'); - const newConnectorIndex = connectorsSection.getElementsByClassName('connector').length; - const newConnectorHTML = ` -
-

Connector ${newConnectorIndex + 1}

-
- - -
-
- - -
-
- `; - - connectorsSection.insertAdjacentHTML('beforeend', newConnectorHTML); - }); - - // Add action functionality - document.getElementById('action_button').addEventListener('click', function() { - const actionsSection = document.getElementById('action_box'); - const ii = actionsSection.getElementsByClassName('action').length; - - const newActionHTML = ` -
-

Action ${ii + 1}

-
- - -
-
- - -
-
- `; - - actionsSection.insertAdjacentHTML('beforeend', newActionHTML); - }); - - // Add MCP functionality - document.getElementById('addMCPButton').addEventListener('click', function() { - const mcpSection = document.getElementById('mcpSection'); - const newMCPIndex = mcpSection.getElementsByClassName('mcp_server').length; - const newMCPHTML = ` -
-

MCP Server ${newMCPIndex + 1}

-
- - -
-
- - -
-
- `; - - mcpSection.insertAdjacentHTML('beforeend', newMCPHTML); - }); - - // Add dynamic prompt functionality - document.getElementById('dynamic_button').addEventListener('click', function() { - const promptsSection = document.getElementById('dynamic_box'); - const ii = promptsSection.getElementsByClassName('promptBlock').length; - - const newPromptHTML = ` -
-

Prompt Block ${ii + 1}

-
- - -
-
- - -
-
- `; - - promptsSection.insertAdjacentHTML('beforeend', newPromptHTML); - }); - - // Form submission handling document.addEventListener('DOMContentLoaded', function() { + // Initialize common form components + initAgentFormCommon({ + actions: actions, + connectors: connectors, + promptBlocks: promptBlocks + }); + + // Form submission handling const form = document.getElementById('create-agent-form'); form.addEventListener('submit', function(e) { @@ -311,158 +60,26 @@ // Show loading state const createButton = document.getElementById('create-button'); const originalButtonText = createButton.innerHTML; + createButton.setAttribute('data-original-text', originalButtonText); createButton.innerHTML = ' Creating...'; createButton.disabled = true; // Build a structured data object const formData = new FormData(form); - const jsonData = {}; + const jsonData = AgentFormUtils.processFormData(formData); - // Process basic form fields - for (const [key, value] of formData.entries()) { - // Skip the array fields (connectors, actions, promptblocks) 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'); - createButton.innerHTML = originalButtonText; - createButton.disabled = false; - return; // Stop form submission - } - } - // 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; - } - } - } + // Process special fields + jsonData.connectors = AgentFormUtils.processConnectors(createButton); + if (jsonData.connectors === null) return; // Validation failed - // Process connectors - KEEP CONFIG AS STRING - const connectors = []; - const connectorsElements = document.getElementsByClassName('connector'); - for (let i = 0; i < connectorsElements.length; i++) { - const typeField = document.getElementById(`connectorType${i}`); - const configField = document.getElementById(`connectorConfig${i}`); - - if (typeField && configField) { - try { - // Validate JSON but send as string - const configValue = configField.value.trim() || '{}'; - // Parse to validate but don't use the parsed object - JSON.parse(configValue); - - connectors.push({ - type: typeField.value, - config: configValue // Send the raw string, not parsed JSON - }); - } catch (err) { - console.error(`Error parsing connector ${i} config:`, err); - showToast(`Error in connector ${i+1} configuration: Invalid JSON`, 'error'); - - createButton.innerHTML = originalButtonText; - createButton.disabled = false; - return; // Stop form submission - } - } - } - jsonData.connectors = connectors; - - // MCP Servers - const mcpServers = []; - const mcpServerElements = document.getElementsByClassName('mcp_server'); - for (let i = 0; i < mcpServerElements.length; i++) { - const urlField = document.getElementById(`mcpURL${i}`); - const tokenField = document.getElementById(`mcpToken${i}`); - - if (urlField && urlField.value.trim()) { - mcpServers.push({ - url: urlField.value.trim(), - token: tokenField ? tokenField.value.trim() : '' - }); - } - } - jsonData.mcp_servers = mcpServers; + jsonData.mcp_servers = AgentFormUtils.processMCPServers(); - // Process actions - KEEP CONFIG AS STRING - const actions = []; - const actionElements = document.getElementsByClassName('action'); - for (let i = 0; i < actionElements.length; i++) { - const nameField = document.getElementById(`actionsName${i}`); - const configField = document.getElementById(`actionsConfig${i}`); - - if (nameField && configField) { - try { - // Validate JSON but send as string - const configValue = configField.value.trim() || '{}'; - // Parse to validate but don't use the parsed object - JSON.parse(configValue); - - actions.push({ - name: nameField.value, - config: configValue // Send the raw string, not parsed JSON - }); - } catch (err) { - console.error(`Error parsing action ${i} config:`, err); - showToast(`Error in action ${i+1} configuration: Invalid JSON`, 'error'); - - createButton.innerHTML = originalButtonText; - createButton.disabled = false; - return; // Stop form submission - } - } - } - jsonData.actions = actions; - - // Process prompt blocks - KEEP CONFIG AS STRING - const promptBlocks = []; - const promptBlockElements = document.getElementsByClassName('promptBlock'); - for (let i = 0; i < promptBlockElements.length; i++) { - const nameField = document.getElementById(`promptName${i}`); - const configField = document.getElementById(`promptConfig${i}`); - - if (nameField && configField) { - try { - // Validate JSON but send as string - const configValue = configField.value.trim() || '{}'; - // Parse to validate but don't use the parsed object - JSON.parse(configValue); - - promptBlocks.push({ - name: nameField.value, - config: configValue // Send the raw string, not parsed JSON - }); - } catch (err) { - console.error(`Error parsing prompt block ${i} config:`, err); - showToast(`Error in prompt block ${i+1} configuration: Invalid JSON`, 'error'); - - createButton.innerHTML = originalButtonText; - createButton.disabled = false; - return; // Stop form submission - } - } - } - jsonData.promptblocks = promptBlocks; - - console.log('Sending data:', jsonData); + jsonData.actions = AgentFormUtils.processActions(createButton); + if (jsonData.actions === null) return; // Validation failed + jsonData.promptblocks = AgentFormUtils.processPromptBlocks(createButton); + if (jsonData.promptblocks === null) return; // Validation failed + // Send the structured data as JSON fetch('/create', { method: 'POST', @@ -490,7 +107,7 @@ // Redirect to agent list page after a delay setTimeout(() => { - window.location.href = '/'; + window.location.href = '/agents'; }, 2000); } else if (data.error) { // Show error toast @@ -529,24 +146,6 @@ }); }); }); - - // Add additional CSS for checkbox labels - document.addEventListener('DOMContentLoaded', function() { - const style = document.createElement('style'); - style.textContent = ` - .checkbox-label { - display: flex; - align-items: center; - cursor: pointer; - margin-bottom: 10px; - } - - .checkbox-label .checkbox-custom { - margin-right: 10px; - } - `; - document.head.appendChild(style); - }); \ No newline at end of file diff --git a/webui/views/partials/agent-form.html b/webui/views/partials/agent-form.html new file mode 100644 index 0000000..4ee5281 --- /dev/null +++ b/webui/views/partials/agent-form.html @@ -0,0 +1,173 @@ +
+ + {{ if .Name }} + + {{ else }} + + {{ end }} +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
\ No newline at end of file diff --git a/webui/views/partials/header.html b/webui/views/partials/header.html index 50005c5..6c5b1a4 100644 --- a/webui/views/partials/header.html +++ b/webui/views/partials/header.html @@ -6,766 +6,5 @@ - - - + + \ No newline at end of file diff --git a/webui/views/settings.html b/webui/views/settings.html index c721307..8138ee6 100644 --- a/webui/views/settings.html +++ b/webui/views/settings.html @@ -3,6 +3,7 @@ Agent settings {{.Name}} {{template "views/partials/header"}} + {{template "views/partials/menu"}} @@ -34,179 +35,15 @@ - +

Edit Agent Configuration

-
- -
- -
- -
+ {{template "views/partials/agent-form" .}} -
- -
- -
- -
- -
- -
- -
- -
- - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
@@ -250,6 +87,13 @@ let agentConfig = null; document.addEventListener('DOMContentLoaded', function() { + // Initialize common form components + initAgentFormCommon({ + actions: actions, + connectors: connectors, + promptBlocks: promptBlocks + }); + // Load agent configuration when page loads loadAgentConfig(); @@ -330,257 +174,34 @@ // Set current date for timestamp const now = new Date(); document.getElementById('current-date').textContent = now.toISOString().split('T')[0]; - - // Add connector functionality - document.getElementById('addConnectorButton').addEventListener('click', function() { - const connectorsSection = document.getElementById('connectorsSection'); - const newConnectorIndex = connectorsSection.getElementsByClassName('connector').length; - const newConnectorHTML = ` -
-

Connector ${newConnectorIndex + 1}

-
- - -
-
- - -
-
- `; - connectorsSection.insertAdjacentHTML('beforeend', newConnectorHTML); - }); - - // Add MCP functionality - document.getElementById('addMCPButton').addEventListener('click', function() { - const mcpSection = document.getElementById('mcpSection'); - const newMCPIndex = mcpSection.getElementsByClassName('mcp_server').length; - const newMCPHTML = ` -
-

MCP Server ${newMCPIndex + 1}

-
- - -
-
- - -
-
- `; - - mcpSection.insertAdjacentHTML('beforeend', newMCPHTML); - }); - - - // Add action functionality - document.getElementById('action_button').addEventListener('click', function() { - const actionsSection = document.getElementById('action_box'); - const ii = actionsSection.getElementsByClassName('action').length; - - const newActionHTML = ` -
-

Action ${ii + 1}

-
- - -
-
- - -
-
- `; - - actionsSection.insertAdjacentHTML('beforeend', newActionHTML); - }); - - // Add dynamic prompt functionality - document.getElementById('dynamic_button').addEventListener('click', function() { - const promptsSection = document.getElementById('dynamic_box'); - const ii = promptsSection.getElementsByClassName('promptBlock').length; - - const newPromptHTML = ` -
-

Prompt Block ${ii + 1}

-
- - -
-
- - -
-
- `; - - promptsSection.insertAdjacentHTML('beforeend', newPromptHTML); - }); - - // Form submission handling - const form = document.getElementById('edit-agent-form'); + // Handle form submission for updating agent + const form = document.getElementById('edit-agent-form'); form.addEventListener('submit', function(e) { e.preventDefault(); // Show a loading state const updateButton = document.getElementById('update-button'); const originalButtonText = updateButton.innerHTML; + updateButton.setAttribute('data-original-text', originalButtonText); updateButton.innerHTML = ' Updating...'; updateButton.disabled = true; // Build a structured data object const formData = new FormData(form); - const jsonData = {}; + const jsonData = AgentFormUtils.processFormData(formData); - // Process basic form fields - for (const [key, value] of formData.entries()) { - // Skip the array fields (connectors, actions, promptblocks) 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'); - updateButton.innerHTML = originalButtonText; - updateButton.disabled = false; - return; // Stop form submission - } - } - // 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; - } - } - } + // Process special fields + jsonData.connectors = AgentFormUtils.processConnectors(updateButton); + if (jsonData.connectors === null) return; // Validation failed - // Process connectors - KEEP CONFIG AS STRING - const connectors = []; - const connectorsElements = document.getElementsByClassName('connector'); - for (let i = 0; i < connectorsElements.length; i++) { - const typeField = document.getElementById(`connectorType${i}`); - const configField = document.getElementById(`connectorConfig${i}`); - - if (typeField && configField) { - try { - // Validate JSON but send as string - const configValue = configField.value.trim() || '{}'; - // Parse to validate but don't use the parsed object - JSON.parse(configValue); - - connectors.push({ - type: typeField.value, - config: configValue // Send the raw string, not parsed JSON - }); - } catch (err) { - console.error(`Error parsing connector ${i} config:`, err); - showToast(`Error in connector ${i+1} configuration: Invalid JSON`, 'error'); - - updateButton.innerHTML = originalButtonText; - updateButton.disabled = false; - return; // Stop form submission - } - } - } - jsonData.connectors = connectors; - - // MCP Servers - updated to handle URL and token - const mcpServers = []; - const mcpServerElements = document.getElementsByClassName('mcp_server'); - for (let i = 0; i < mcpServerElements.length; i++) { - const urlField = document.getElementById(`mcpURL${i}`); - const tokenField = document.getElementById(`mcpToken${i}`); - - if (urlField && urlField.value.trim()) { - mcpServers.push({ - url: urlField.value.trim(), - token: tokenField ? tokenField.value.trim() : '' - }); - } - } - jsonData.mcp_servers = mcpServers; - - // Process actions - KEEP CONFIG AS STRING - const actions = []; - const actionElements = document.getElementsByClassName('action'); - for (let i = 0; i < actionElements.length; i++) { - const nameField = document.getElementById(`actionsName${i}`); - const configField = document.getElementById(`actionsConfig${i}`); - - if (nameField && configField) { - try { - // Validate JSON but send as string - const configValue = configField.value.trim() || '{}'; - // Parse to validate but don't use the parsed object - JSON.parse(configValue); - - actions.push({ - name: nameField.value, - config: configValue // Send the raw string, not parsed JSON - }); - } catch (err) { - console.error(`Error parsing action ${i} config:`, err); - showToast(`Error in action ${i+1} configuration: Invalid JSON`, 'error'); - - updateButton.innerHTML = originalButtonText; - updateButton.disabled = false; - return; // Stop form submission - } - } - } - jsonData.actions = actions; + jsonData.mcp_servers = AgentFormUtils.processMCPServers(); - // Process prompt blocks - KEEP CONFIG AS STRING - const promptBlocks = []; - const promptBlockElements = document.getElementsByClassName('promptBlock'); - for (let i = 0; i < promptBlockElements.length; i++) { - const nameField = document.getElementById(`promptName${i}`); - const configField = document.getElementById(`promptConfig${i}`); - - if (nameField && configField) { - try { - // Validate JSON but send as string - const configValue = configField.value.trim() || '{}'; - // Parse to validate but don't use the parsed object - JSON.parse(configValue); - - promptBlocks.push({ - name: nameField.value, - config: configValue // Send the raw string, not parsed JSON - }); - } catch (err) { - console.error(`Error parsing prompt block ${i} config:`, err); - showToast(`Error in prompt block ${i+1} configuration: Invalid JSON`, 'error'); - - updateButton.innerHTML = originalButtonText; - updateButton.disabled = false; - return; // Stop form submission - } - } - } - jsonData.promptblocks = promptBlocks; + jsonData.actions = AgentFormUtils.processActions(updateButton); + if (jsonData.actions === null) return; // Validation failed + + jsonData.promptblocks = AgentFormUtils.processPromptBlocks(updateButton); + if (jsonData.promptblocks === null) return; // Validation failed console.log('Sending data:', jsonData); @@ -697,8 +318,8 @@ }); } - - function populateFormWithConfig(config) { + // Populate form with agent configuration + function populateFormWithConfig(config) { // Clear existing dynamic sections document.getElementById('connectorsSection').innerHTML = ''; document.getElementById('mcpSection').innerHTML = ''; @@ -735,12 +356,12 @@ // Set values if (connectorType) { - setSelectValue(connectorType, connector.type); + AgentFormUtils.setSelectValue(connectorType, connector.type); } if (connectorConfig) { // Format the config value - formatConfigValue(connectorConfig, connector.config); + AgentFormUtils.formatConfigValue(connectorConfig, connector.config); } }); } @@ -785,12 +406,12 @@ // Set values if (actionName) { - setSelectValue(actionName, action.name); + AgentFormUtils.setSelectValue(actionName, action.name); } if (actionConfig) { // Format the config value - formatConfigValue(actionConfig, action.config); + AgentFormUtils.formatConfigValue(actionConfig, action.config); } }); } @@ -807,73 +428,16 @@ // Set values if (promptName) { - setSelectValue(promptName, block.name); + AgentFormUtils.setSelectValue(promptName, block.name); } if (promptConfig) { // Format the config value - formatConfigValue(promptConfig, block.config); + AgentFormUtils.formatConfigValue(promptConfig, block.config); } }); } } - - // Helper function to format config values - function formatConfigValue(configElement, configValue) { - // If it's a string (already stringified JSON), try to parse it first - if (typeof configValue === 'string') { - try { - const parsed = JSON.parse(configValue); - configElement.value = JSON.stringify(parsed, null, 2); - } catch (e) { - console.warn("Failed to parse config JSON string:", e); - configElement.value = configValue; // Keep as is if parsing fails - } - } else if (configValue !== undefined && configValue !== null) { - // Direct object, just stringify with formatting - configElement.value = JSON.stringify(configValue, null, 2); - } else { - // Default to empty object - configElement.value = '{}'; - } - } - - // Helper function to set select value (with fallback if option doesn't exist) - function setSelectValue(selectElement, value) { - // Check if the option exists - const optionExists = Array.from(selectElement.options).some(option => option.value === value); - - if (optionExists) { - selectElement.value = value; - } else if (value) { - // If value is provided but option doesn't exist, create a new option - const newOption = document.createElement('option'); - newOption.value = value; - newOption.text = value + ' (custom)'; - selectElement.add(newOption); - selectElement.value = value; - } - } - - - \ No newline at end of file