diff --git a/webui/app.go b/webui/app.go
index 64fe937..d85c77f 100644
--- a/webui/app.go
+++ b/webui/app.go
@@ -25,6 +25,7 @@ import (
"github.com/donseba/go-htmx"
fiber "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/template/html/v2"
)
type (
@@ -37,10 +38,13 @@ type (
func NewApp(opts ...Option) *App {
config := NewConfig(opts...)
+ engine := html.NewFileSystem(http.FS(viewsfs), ".html")
// Initialize a new Fiber app
// Pass the engine to the Views
- webapp := fiber.New(fiber.Config{})
+ webapp := fiber.New(fiber.Config{
+ Views: engine,
+ })
a := &App{
htmx: htmx.New(),
@@ -245,9 +249,67 @@ func (a *App) ImportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error {
}
}
-// Chat provides a JSON-based API for chat functionality
-// This is designed to work better with the React UI
func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
+ return func(c *fiber.Ctx) error {
+ payload := struct {
+ Message string `json:"message"`
+ }{}
+
+ if err := c.BodyParser(&payload); err != nil {
+ return err
+ }
+ agentName := c.Params("name")
+ manager := pool.GetManager(agentName)
+
+ query := strings.Clone(payload.Message)
+ if query == "" {
+ _, _ = c.Write([]byte("Please enter a message."))
+ return nil
+ }
+ manager.Send(
+ sse.NewMessage(
+ chatDiv(query, "gray"),
+ ).WithEvent("messages"))
+
+ go func() {
+ a := pool.GetAgent(agentName)
+ if a == nil {
+ xlog.Info("Agent not found in pool", c.Params("name"))
+ return
+ }
+ res := a.Ask(
+ coreTypes.WithText(query),
+ )
+ if res.Error != nil {
+ xlog.Error("Error asking agent", "agent", agentName, "error", res.Error)
+ } else {
+ xlog.Info("we got a response from the agent", "agent", agentName, "response", res.Response)
+ }
+ manager.Send(
+ sse.NewMessage(
+ chatDiv(res.Response, "blue"),
+ ).WithEvent("messages"))
+ manager.Send(
+ sse.NewMessage(
+ disabledElement("inputMessage", false), // show again the input
+ ).WithEvent("message_status"))
+
+ //result := `done`
+ // _, _ = w.Write([]byte(result))
+ }()
+
+ manager.Send(
+ sse.NewMessage(
+ loader() + disabledElement("inputMessage", true),
+ ).WithEvent("message_status"))
+
+ return nil
+ }
+}
+
+// ChatAPI provides a JSON-based API for chat functionality
+// This is designed to work better with the React UI
+func (a *App) ChatAPI(pool *state.AgentPool) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
// Parse the request body
payload := struct {
diff --git a/webui/elements.go b/webui/elements.go
new file mode 100644
index 0000000..9a92b5b
--- /dev/null
+++ b/webui/elements.go
@@ -0,0 +1,39 @@
+package webui
+
+import (
+ "fmt"
+ "strings"
+
+ elem "github.com/chasefleming/elem-go"
+ "github.com/chasefleming/elem-go/attrs"
+)
+
+func chatDiv(content string, color string) string {
+ div := elem.Div(attrs.Props{
+ // attrs.ID: "container",
+ attrs.Class: fmt.Sprintf("p-2 my-2 rounded bg-%s-600", color),
+ },
+ elem.Raw(htmlIfy(content)),
+ )
+ return div.Render()
+}
+
+func loader() string {
+ return elem.Div(attrs.Props{
+ attrs.Class: "loader",
+ }).Render()
+}
+
+func disabledElement(id string, disabled bool) string {
+ return elem.Script(nil,
+ elem.If(disabled,
+ elem.Raw(`document.getElementById('`+id+`').disabled = true`),
+ elem.Raw(`document.getElementById('`+id+`').disabled = false`),
+ )).Render()
+}
+
+func htmlIfy(s string) string {
+ s = strings.TrimSpace(s)
+ s = strings.ReplaceAll(s, "\n", "
")
+ return s
+}
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/css/wizard.css b/webui/public/css/wizard.css
new file mode 100644
index 0000000..e6ddff7
--- /dev/null
+++ b/webui/public/css/wizard.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/webui/public/dash.png b/webui/public/dash.png
new file mode 100644
index 0000000..abc8dab
Binary files /dev/null and b/webui/public/dash.png differ
diff --git a/webui/public/dash2.png b/webui/public/dash2.png
new file mode 100644
index 0000000..627a432
Binary files /dev/null and b/webui/public/dash2.png differ
diff --git a/webui/public/js/agent-form.js b/webui/public/js/agent-form.js
new file mode 100644
index 0000000..5c73698
--- /dev/null
+++ b/webui/public/js/agent-form.js
@@ -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 = `
+
Test and execute actions directly from the UI
+Manage and interact with your AI agents
+Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.
+ +Generating agent profiles...
+Select the agents you want to create. You can customize their details before creation.
+ +Configure common settings for all selected agents. These settings will be applied to each agent.
+ + + + +
+ View and manage your list of agents, including detailed profiles and statistics.
+Create a new intelligent agent with custom behaviors, connectors, and actions.
+View performance metrics and insights from your agent operations.
+ Coming Soon +Configure system preferences and global settings for all agents.
+ Coming Soon +Please enter your access token to continue
+Current time (UTC): {{.CurrentDate}}
+Export your agent configuration for backup or transfer.
+ +Permanently delete this agent and all associated data. This action cannot be undone.
+ +