chore(ui): Nuke original web UI, in favor of React
This commit is contained in:
68
webui/app.go
68
webui/app.go
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
"github.com/donseba/go-htmx"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/template/html/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -35,13 +34,10 @@ 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{
|
||||
Views: engine,
|
||||
})
|
||||
webapp := fiber.New(fiber.Config{})
|
||||
|
||||
a := &App{
|
||||
htmx: htmx.New(),
|
||||
@@ -246,67 +242,9 @@ func (a *App) ImportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
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 := `<i>done</i>`
|
||||
// _, _ = 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
|
||||
// Chat 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 {
|
||||
func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// Parse the request body
|
||||
payload := struct {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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", "<br>")
|
||||
return s
|
||||
}
|
||||
@@ -1,714 +0,0 @@
|
||||
: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;
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
@@ -1,564 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// 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';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,475 +0,0 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
};
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 886 KiB |
103
webui/routes.go
103
webui/routes.go
@@ -19,12 +19,6 @@ import (
|
||||
"github.com/mudler/LocalAgent/services"
|
||||
)
|
||||
|
||||
//go:embed views/*
|
||||
var viewsfs embed.FS
|
||||
|
||||
//go:embed public/*
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
//go:embed react-ui/dist/*
|
||||
var reactUI embed.FS
|
||||
|
||||
@@ -37,12 +31,6 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
Browse: true,
|
||||
}))
|
||||
|
||||
/* webapp.Use("/public", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(embeddedFiles),
|
||||
PathPrefix: "public",
|
||||
Browse: true,
|
||||
}))
|
||||
*/
|
||||
if len(app.config.ApiKeys) > 0 {
|
||||
kaConfig, err := GetKeyAuthConfig(app.config.ApiKeys)
|
||||
if err != nil || kaConfig == nil {
|
||||
@@ -51,15 +39,9 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
webapp.Use(v2keyauth.New(*kaConfig))
|
||||
}
|
||||
|
||||
/* webapp.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/index", fiber.Map{
|
||||
"Agents": pool.List(),
|
||||
"AgentCount": len(pool.List()),
|
||||
"Actions": len(services.AvailableActions),
|
||||
"Connectors": len(services.AvailableConnectors),
|
||||
webapp.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Redirect("/app")
|
||||
})
|
||||
})
|
||||
*/
|
||||
webapp.Use("/app", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(reactUI),
|
||||
PathPrefix: "react-ui/dist",
|
||||
@@ -75,31 +57,6 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
return c.Send(indexHTML)
|
||||
})
|
||||
|
||||
/* webapp.Get("/agents", func(c *fiber.Ctx) error {
|
||||
statuses := map[string]bool{}
|
||||
for _, a := range pool.List() {
|
||||
agent := pool.GetAgent(a)
|
||||
if agent == nil {
|
||||
xlog.Error("Agent not found", "name", a)
|
||||
continue
|
||||
}
|
||||
statuses[a] = !agent.Paused()
|
||||
}
|
||||
return c.Render("views/agents", fiber.Map{
|
||||
"Agents": pool.List(),
|
||||
"Status": statuses,
|
||||
})
|
||||
})
|
||||
*/
|
||||
/* webapp.Get("/api/create", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/create", fiber.Map{
|
||||
"Actions": services.AvailableActions,
|
||||
"Connectors": services.AvailableConnectors,
|
||||
"PromptBlocks": services.AvailableBlockPrompts,
|
||||
})
|
||||
})
|
||||
*/
|
||||
// Define a route for the GET method on the root path '/'
|
||||
webapp.Get("/sse/:name", func(c *fiber.Ctx) error {
|
||||
m := pool.GetManager(c.Params("name"))
|
||||
if m == nil {
|
||||
@@ -110,68 +67,16 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
return nil
|
||||
})
|
||||
|
||||
/*webapp.Get("/status/:name", func(c *fiber.Ctx) error {
|
||||
history := pool.GetStatusHistory(c.Params("name"))
|
||||
if history == nil {
|
||||
history = &state.Status{ActionResults: []types.ActionState{}}
|
||||
}
|
||||
// reverse history
|
||||
|
||||
return c.Render("views/status", fiber.Map{
|
||||
"Name": c.Params("name"),
|
||||
"History": Reverse(history.Results()),
|
||||
})
|
||||
})
|
||||
|
||||
webapp.Get("/notify/:name", app.Notify(pool))
|
||||
webapp.Post("/chat/:name", app.Chat(pool))
|
||||
*/
|
||||
webapp.Post("/api/agent/create", app.Create(pool))
|
||||
webapp.Delete("/api/agent/:name", app.Delete(pool))
|
||||
webapp.Put("/api/agent/:name/pause", app.Pause(pool))
|
||||
webapp.Put("/api/agent/:name/start", app.Start(pool))
|
||||
|
||||
// Add JSON-based chat API endpoint
|
||||
webapp.Post("/api/chat/:name", app.ChatAPI(pool))
|
||||
webapp.Post("/api/chat/:name", app.Chat(pool))
|
||||
webapp.Post("/api/notify/:name", app.Notify(pool))
|
||||
|
||||
webapp.Post("/v1/responses", app.Responses(pool))
|
||||
|
||||
/* webapp.Get("/talk/:name", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/chat", fiber.Map{
|
||||
// "Character": agent.Character,
|
||||
"Name": c.Params("name"),
|
||||
})
|
||||
})
|
||||
|
||||
webapp.Get("/settings/:name", func(c *fiber.Ctx) error {
|
||||
status := false
|
||||
for _, a := range pool.List() {
|
||||
if a == c.Params("name") {
|
||||
status = !pool.GetAgent(a).Paused()
|
||||
}
|
||||
}
|
||||
|
||||
return c.Render("views/settings", fiber.Map{
|
||||
"Name": c.Params("name"),
|
||||
"Status": status,
|
||||
"Actions": services.AvailableActions,
|
||||
"Connectors": services.AvailableConnectors,
|
||||
"PromptBlocks": services.AvailableBlockPrompts,
|
||||
})
|
||||
})
|
||||
|
||||
webapp.Get("/actions-playground", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/actions", fiber.Map{})
|
||||
})
|
||||
|
||||
webapp.Get("/group-create", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/group-create", fiber.Map{
|
||||
"Actions": services.AvailableActions,
|
||||
"Connectors": services.AvailableConnectors,
|
||||
"PromptBlocks": services.AvailableBlockPrompts,
|
||||
})
|
||||
})
|
||||
*/
|
||||
// New API endpoints for getting and updating agent configuration
|
||||
webapp.Get("/api/agent/:name/config", app.GetAgentConfig(pool))
|
||||
webapp.Put("/api/agent/:name/config", app.UpdateAgentConfig(pool))
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Actions Playground</title>
|
||||
{{template "views/partials/header"}}
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
|
||||
<!-- Toast for notifications -->
|
||||
<div id="toast" class="toast">
|
||||
<span id="toast-message"></span>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl md:text-6xl font-bold">Actions Playground</h1>
|
||||
<p class="mt-4 text-gray-400">Test and execute actions directly from the UI</p>
|
||||
</header>
|
||||
|
||||
<section class="section-box mb-8">
|
||||
<h2 class="mb-4">Select an Action</h2>
|
||||
<div class="mb-4">
|
||||
<label for="action-select" class="block mb-2">Available Actions:</label>
|
||||
<select id="action-select" class="w-full">
|
||||
<option value="">-- Select an action --</option>
|
||||
<!-- Actions will be loaded here -->
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="config-section" class="section-box mb-8 hidden">
|
||||
<h2 class="mb-4">Action Configuration</h2>
|
||||
<form id="action-form">
|
||||
<div class="mb-6">
|
||||
<label for="config-json" class="block mb-2">Configuration (JSON):</label>
|
||||
<textarea id="config-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="params-json" class="block mb-2">Parameters (JSON):</label>
|
||||
<textarea id="params-json" class="w-full" rows="5" placeholder='{"key": "value"}'>{}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="action-btn start-btn">
|
||||
<i class="fas fa-play"></i> Execute Action
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="results-section" class="section-box mb-8 hidden">
|
||||
<h2 class="mb-4">Action Results</h2>
|
||||
<div id="action-results">
|
||||
<!-- Results will appear here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="text-center text-gray-500 text-sm mb-8">
|
||||
<p>© 2025 LocalAgent.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load available actions
|
||||
fetchActions();
|
||||
|
||||
// Handle action selection
|
||||
document.getElementById('action-select').addEventListener('change', function() {
|
||||
const actionId = this.value;
|
||||
if (actionId) {
|
||||
document.getElementById('config-section').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('config-section').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Hide results when changing actions
|
||||
document.getElementById('results-section').classList.add('hidden');
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('action-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const actionId = document.getElementById('action-select').value;
|
||||
if (actionId) {
|
||||
executeAction(actionId);
|
||||
} else {
|
||||
showToast('Please select an action first', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function fetchActions() {
|
||||
fetch('/actions')
|
||||
.then(response => response.json())
|
||||
.then(actions => {
|
||||
const select = document.getElementById('action-select');
|
||||
|
||||
// Clear existing options except the first one
|
||||
while (select.options.length > 1) {
|
||||
select.remove(1);
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.text = 'No actions available';
|
||||
option.disabled = true;
|
||||
select.add(option);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add options for each action
|
||||
actions.forEach(actionId => {
|
||||
const option = document.createElement('option');
|
||||
option.value = actionId;
|
||||
option.text = actionId; // Using actionId as display text
|
||||
select.add(option);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching actions:', error);
|
||||
showToast('Failed to load actions: ' + error.message, 'error');
|
||||
|
||||
const select = document.getElementById('action-select');
|
||||
const option = document.createElement('option');
|
||||
option.text = 'Error loading actions';
|
||||
option.disabled = true;
|
||||
|
||||
// Clear existing options except the first one
|
||||
while (select.options.length > 1) {
|
||||
select.remove(1);
|
||||
}
|
||||
|
||||
select.add(option);
|
||||
});
|
||||
}
|
||||
|
||||
function executeAction(actionId) {
|
||||
// Get the JSON data from textareas
|
||||
let config = {};
|
||||
let params = {};
|
||||
|
||||
try {
|
||||
const configText = document.getElementById('config-json').value.trim();
|
||||
if (configText && configText !== '{}') {
|
||||
config = JSON.parse(configText);
|
||||
}
|
||||
|
||||
const paramsText = document.getElementById('params-json').value.trim();
|
||||
if (paramsText && paramsText !== '{}') {
|
||||
params = JSON.parse(paramsText);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Invalid JSON: ' + error.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the results section with loading indicator
|
||||
const resultsSection = document.getElementById('results-section');
|
||||
resultsSection.classList.remove('hidden');
|
||||
|
||||
const resultDiv = document.getElementById('action-results');
|
||||
resultDiv.innerHTML = `
|
||||
<div class="flex justify-center items-center py-8">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Execute the action
|
||||
fetch(`/action/${actionId}/run`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config: config,
|
||||
params: params
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
// Display the results
|
||||
showActionResult(result);
|
||||
showToast('Action executed successfully!', 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-error" style="display: block;">
|
||||
<i class="fas fa-exclamation-circle mr-2"></i> Error: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
showToast('Error executing action', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function showActionResult(result) {
|
||||
const resultDiv = document.getElementById('action-results');
|
||||
|
||||
let html = '';
|
||||
|
||||
// Display result
|
||||
if (result.Result) {
|
||||
html += `
|
||||
<div class="mb-4">
|
||||
<h4 class="text-lg mb-2" style="color: var(--secondary);">Result:</h4>
|
||||
<div class="code-terminal">
|
||||
<pre>${escapeHtml(result.Result)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Display metadata if available
|
||||
if (result.Metadata && Object.keys(result.Metadata).length > 0) {
|
||||
html += `
|
||||
<div class="mb-4">
|
||||
<h4 class="text-lg mb-2" style="color: var(--secondary);">Metadata:</h4>
|
||||
<div class="code-terminal">
|
||||
<pre>${escapeHtml(JSON.stringify(result.Metadata, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<p class="text-gray-400">No results returned from the action.</p>';
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
|
||||
// Scroll to results
|
||||
resultDiv.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
const toast = document.getElementById('toast');
|
||||
const toastMessage = document.getElementById('toast-message');
|
||||
|
||||
toastMessage.textContent = message;
|
||||
toast.className = 'toast toast-' + type;
|
||||
toast.classList.add('toast-visible');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('toast-visible');
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 5px solid var(--tertiary);
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.code-terminal {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,332 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agent List</title>
|
||||
{{template "views/partials/header"}}
|
||||
<style>
|
||||
.avatar-placeholder {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #2a2a2a, #3a3a3a);
|
||||
color: var(--primary);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 2px solid var(--primary);
|
||||
box-shadow: var(--neon-glow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-placeholder::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--primary);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
animation: loading-progress 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes loading-progress {
|
||||
0% { width: 0; }
|
||||
50% { width: 100%; }
|
||||
100% { width: 0; }
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
|
||||
<!-- Toast for notifications -->
|
||||
<div id="toast" class="toast">
|
||||
<span id="toast-message"></span>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl md:text-6xl font-bold">Agent List</h1>
|
||||
<p class="mt-4 text-gray-400">Manage and interact with your AI agents</p>
|
||||
</header>
|
||||
|
||||
<div class="button-container justify-center mb-6">
|
||||
<a href="/create" class="action-btn start-btn">
|
||||
<i class="fas fa-plus-circle"></i> Add New Agent
|
||||
</a>
|
||||
<button id="toggle-import" class="action-btn" style="background: linear-gradient(135deg, var(--tertiary), #4a76a8);">
|
||||
<i class="fas fa-file-import"></i> Import Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section id="import-section" class="hidden mb-8">
|
||||
<div class="section-box">
|
||||
<h2>Import Agent</h2>
|
||||
|
||||
<!-- Response Messages Container -->
|
||||
<div id="response-container">
|
||||
<!-- Success Alert -->
|
||||
<div id="success-alert" class="alert alert-success" style="display: none;">
|
||||
Agent imported successfully! The page will refresh in a moment.
|
||||
</div>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<div id="error-alert" class="alert alert-error" style="display: none;">
|
||||
<span id="error-message">Error importing agent.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id='import-form' hx-encoding='multipart/form-data' hx-post='/settings/import' hx-target="#response-container" hx-swap="none">
|
||||
<div class="mb-4">
|
||||
<label for="file" class="block mb-2">Select Agent File:</label>
|
||||
<input type='file' name='file' id='file'>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<button id="import-button" type="submit" class="action-btn">
|
||||
<i class="fas fa-cloud-upload-alt"></i> Import Agent
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{{ $status := .Status }}
|
||||
{{ range .Agents }}
|
||||
<div hx-ext="sse" data-agent-name="{{.}}" class="card">
|
||||
<div class="flex flex-col items-center text-center p-4">
|
||||
<div class="avatar-container mb-4">
|
||||
<img src="/avatars/{{.}}.png" alt="{{.}}" class="w-24 h-24 rounded-full"
|
||||
style="border: 2px solid var(--primary); box-shadow: var(--neon-glow); display: none;"
|
||||
onload="this.style.display = 'block'; this.nextElementSibling.style.display = 'none';"
|
||||
onerror="this.style.display = 'none'; this.nextElementSibling.style.display = 'flex';">
|
||||
<div class="avatar-placeholder">
|
||||
<span class="placeholder-text"><i class="fas fa-sync fa-spin"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<h2>{{.}}</h2>
|
||||
<div class="mb-4 flex items-center justify-center">
|
||||
<span class="badge {{ if eq (index $status .) true }}badge-primary{{ else }}badge-secondary{{ end }} mr-2">
|
||||
{{ if eq (index $status .) true }}Active{{ else }}Inactive{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 w-full mb-4">
|
||||
<a href="/status/{{.}}" class="action-btn flex items-center justify-center"
|
||||
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
|
||||
<i class="fas fa-info-circle mr-2"></i> Status
|
||||
</a>
|
||||
<a href="/talk/{{.}}" class="action-btn flex items-center justify-center"
|
||||
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
|
||||
<i class="fas fa-comments mr-2"></i> Talk
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 w-full">
|
||||
<button class="action-btn toggle-btn col-span-1"
|
||||
data-agent="{{.}}"
|
||||
data-active="{{ if eq (index $status .) true }}true{{ else }}false{{ end }}">
|
||||
{{ if eq (index $status .) true }}
|
||||
<i class="fas fa-pause"></i> Pause
|
||||
{{ else }}
|
||||
<i class="fas fa-play"></i> Start
|
||||
{{ end }}
|
||||
</button>
|
||||
<a href="/settings/{{.}}" class="action-btn col-span-1 flex items-center justify-center"
|
||||
style="background: linear-gradient(135deg, #2a2a2a, #3a3a3a);">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</section>
|
||||
|
||||
<div class="user-info mt-8 mb-4">
|
||||
<span></span>
|
||||
<span class="timestamp"></span>
|
||||
</div>
|
||||
|
||||
<footer class="text-center text-gray-500 text-sm mb-8">
|
||||
<p>© 2025 LocalAgent.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Image loading handler
|
||||
document.querySelectorAll('.avatar-container img').forEach(img => {
|
||||
// Check if image is already cached
|
||||
if (img.complete) {
|
||||
if (img.naturalHeight === 0) {
|
||||
// Image failed to load
|
||||
img.style.display = 'none';
|
||||
img.nextElementSibling.style.display = 'flex';
|
||||
} else {
|
||||
// Image loaded successfully
|
||||
img.style.display = 'block';
|
||||
img.nextElementSibling.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// onload and onerror handlers are already in the HTML
|
||||
});
|
||||
|
||||
const importSection = document.getElementById('import-section');
|
||||
const toggleImport = document.getElementById('toggle-import');
|
||||
|
||||
// Toggle import section visibility
|
||||
toggleImport.addEventListener('click', function() {
|
||||
importSection.classList.toggle('hidden');
|
||||
// Add glitch effect when showing
|
||||
if (!importSection.classList.contains('hidden')) {
|
||||
importSection.style.animation = 'glitch 0.3s';
|
||||
setTimeout(() => {
|
||||
importSection.style.animation = '';
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle import form submission
|
||||
document.getElementById('import-form').addEventListener('htmx:afterRequest', function(event) {
|
||||
const xhr = event.detail.xhr;
|
||||
const successAlert = document.getElementById('success-alert');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// Hide both alerts initially
|
||||
successAlert.style.display = 'none';
|
||||
errorAlert.style.display = 'none';
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.status === "ok") {
|
||||
// Show success message
|
||||
successAlert.style.display = 'block';
|
||||
showToast("Agent imported successfully!", "success");
|
||||
|
||||
// Refresh the page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else if (response.error) {
|
||||
// Show error message
|
||||
errorMessage.textContent = response.error;
|
||||
errorAlert.style.display = 'block';
|
||||
showToast("Import failed: " + response.error, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle parsing error
|
||||
errorMessage.textContent = "Invalid response format";
|
||||
errorAlert.style.display = 'block';
|
||||
showToast("Invalid response format", "error");
|
||||
}
|
||||
} else {
|
||||
// Handle HTTP error
|
||||
errorMessage.textContent = "Server error: " + xhr.status;
|
||||
errorAlert.style.display = 'block';
|
||||
showToast("Server error: " + xhr.status, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// Handle toggle buttons - using pure JavaScript
|
||||
document.querySelectorAll('.toggle-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const agent = this.getAttribute('data-agent');
|
||||
const isActive = this.getAttribute('data-active') === 'true';
|
||||
const endpoint = isActive ? `/pause/${agent}` : `/start/${agent}`;
|
||||
|
||||
// Add animation
|
||||
this.style.animation = 'pulse 0.5s';
|
||||
|
||||
// Create a new XMLHttpRequest
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('PUT', endpoint);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onload = () => {
|
||||
// Clear animation
|
||||
this.style.animation = '';
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.status === "ok") {
|
||||
// Toggle the button state
|
||||
const newState = !isActive;
|
||||
this.setAttribute('data-active', newState ? 'true' : 'false');
|
||||
|
||||
// Update button text and icon
|
||||
if (newState) {
|
||||
this.innerHTML = '<i class="fas fa-pause"></i> Pause';
|
||||
} else {
|
||||
this.innerHTML = '<i class="fas fa-play"></i> Start';
|
||||
}
|
||||
|
||||
// Show success toast
|
||||
const action = isActive ? 'pause' : 'start';
|
||||
showToast(`Agent "${agent}" ${action}ed successfully`, 'success');
|
||||
|
||||
// Update the status badge
|
||||
updateAgentStatus(agent, newState);
|
||||
} else if (response.error) {
|
||||
// Show error toast
|
||||
showToast(`Error: ${response.error}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle parsing error
|
||||
showToast("Invalid response format", 'error');
|
||||
console.error("Error parsing response:", e);
|
||||
}
|
||||
} else {
|
||||
// Handle HTTP error
|
||||
showToast(`Server error: ${xhr.status}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
// Clear animation
|
||||
this.style.animation = '';
|
||||
showToast("Network error occurred", 'error');
|
||||
console.error("Network error occurred");
|
||||
};
|
||||
|
||||
// Send the request
|
||||
xhr.send(JSON.stringify({}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Function to update agent status in the UI
|
||||
function updateAgentStatus(agentName, isOnline) {
|
||||
// Find the card for this agent
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach(card => {
|
||||
const agentTitle = card.querySelector('h2').textContent;
|
||||
if (agentTitle === agentName) {
|
||||
// Update the badge
|
||||
const badge = card.querySelector('.badge');
|
||||
if (isOnline) {
|
||||
badge.className = 'badge badge-primary mr-2';
|
||||
badge.textContent = 'Active';
|
||||
} else {
|
||||
badge.className = 'badge badge-secondary mr-2';
|
||||
badge.textContent = 'Inactive';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,91 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Smart Agent Interface</title>
|
||||
{{template "views/partials/header"}}
|
||||
<style>
|
||||
body { overflow: hidden; }
|
||||
.chat-container { height: 90vh; display: flex; flex-direction: column; }
|
||||
.chat-messages { overflow-y: auto; flex-grow: 1; }
|
||||
.htmx-indicator{
|
||||
opacity:0;
|
||||
transition: opacity 10ms ease-in;
|
||||
}
|
||||
.htmx-request .htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
/* Loader (https://cssloaders.github.io/) */
|
||||
.loader {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
margin:15px auto;
|
||||
position: relative;
|
||||
color: #FFF;
|
||||
box-sizing: border-box;
|
||||
animation: animloader 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
|
||||
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
|
||||
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 p-4 text-white font-sans" hx-ext="sse" sse-connect="/sse/{{.Name}}">
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="chat-container bg-gray-800 shadow-lg rounded-lg" >
|
||||
|
||||
<!-- Chat Header -->
|
||||
<div class="border-b border-gray-700 p-4">
|
||||
<h1 class="text-lg font-semibold">Talk to '{{.Name}}'</h1>
|
||||
</div>
|
||||
|
||||
<!-- Chat Messages -->
|
||||
<div class="chat-messages p-4">
|
||||
<!-- Client Box -->
|
||||
<div class="bg-gray-700 p-4">
|
||||
<h2 class="text-sm font-semibold">Clients:</h2>
|
||||
<div id="clients" class="text-sm text-gray-300">
|
||||
<!-- Status updates dynamically here -->
|
||||
<div sse-swap="clients"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- HUD Box -->
|
||||
<div class="bg-gray-700 p-4">
|
||||
<h2 class="text-sm font-semibold">Status:</h2>
|
||||
<div id="hud" class="text-sm text-gray-300">
|
||||
<!-- Status updates dynamically here -->
|
||||
<div sse-swap="hud"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div sse-swap="messages" hx-swap="beforeend" id="messages" hx-on:htmx:after-settle="document.getElementById('messages').scrollIntoView(false)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Status Box -->
|
||||
<div class="bg-gray-700 p-4">
|
||||
<h2 class="text-sm font-semibold">Agent:</h2>
|
||||
<div id="agentStatus" class="text-sm text-gray-300">
|
||||
<!-- Status updates dynamically here -->
|
||||
<div sse-swap="status" ></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="p-4 border-t border-gray-700">
|
||||
<div sse-swap="message_status"></div>
|
||||
<input id="inputMessage" name="message" type="text" hx-post="/chat/{{.Name}}" hx-target="#results" hx-indicator=".htmx-indicator"
|
||||
class="p-2 border rounded w-full bg-gray-600 text-white placeholder-gray-300" placeholder="Type a message..." _="on htmx:afterRequest set my value to ''">
|
||||
<div class="my-2 htmx-indicator" ></div>
|
||||
<div id="results" class="flex justify-center"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,154 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Create New Agent</title>
|
||||
{{template "views/partials/header"}}
|
||||
<script src="/public/js/wizard.js"></script>
|
||||
<link rel="stylesheet" href="/public/css/wizard.css">
|
||||
<script src="/public/js/connector-templates.js"></script>
|
||||
<script src="/public/js/agent-form.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="container">
|
||||
<div class="section-box">
|
||||
<h1>Create New Agent</h1>
|
||||
|
||||
<form id="create-agent-form" action="/create" method="POST">
|
||||
{{template "views/partials/agent-form" . }}
|
||||
|
||||
<button type="submit" id="create-button" data-original-text="<i class='fas fa-robot'></i> Create Agent">
|
||||
<i class="fas fa-robot"></i> Create Agent
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Response Messages Container -->
|
||||
<div id="response-container">
|
||||
<!-- Alert messages will be shown here -->
|
||||
<div id="success-alert" class="alert alert-success" style="display: none;">
|
||||
Agent created successfully! Redirecting to agent list...
|
||||
</div>
|
||||
|
||||
<div id="error-alert" class="alert alert-error" style="display: none;">
|
||||
<span id="error-message">Error creating agent.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast notification container -->
|
||||
<div id="toast" class="toast">
|
||||
<span id="toast-message"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
|
||||
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) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading state
|
||||
const createButton = document.getElementById('create-button');
|
||||
const originalButtonText = createButton.innerHTML;
|
||||
createButton.setAttribute('data-original-text', originalButtonText);
|
||||
createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
|
||||
createButton.disabled = true;
|
||||
|
||||
// Build a structured data object
|
||||
const formData = new FormData(form);
|
||||
const jsonData = AgentFormUtils.processFormData(formData);
|
||||
|
||||
// Process special fields
|
||||
jsonData.connectors = AgentFormUtils.processConnectors(createButton);
|
||||
if (jsonData.connectors === null) return; // Validation failed
|
||||
|
||||
jsonData.mcp_servers = AgentFormUtils.processMCPServers();
|
||||
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(jsonData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const successAlert = document.getElementById('success-alert');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// Hide both alerts initially
|
||||
successAlert.style.display = 'none';
|
||||
errorAlert.style.display = 'none';
|
||||
|
||||
if (data.status === "ok") {
|
||||
// Show success toast
|
||||
showToast('Agent created successfully!', 'success');
|
||||
|
||||
// Show success message
|
||||
successAlert.style.display = 'block';
|
||||
|
||||
// Redirect to agent list page after a delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/agents';
|
||||
}, 2000);
|
||||
} else if (data.error) {
|
||||
// Show error toast
|
||||
showToast('Error: ' + data.error, 'error');
|
||||
|
||||
// Show error message
|
||||
errorMessage.textContent = data.error;
|
||||
errorAlert.style.display = 'block';
|
||||
|
||||
// Restore button state
|
||||
createButton.innerHTML = originalButtonText;
|
||||
createButton.disabled = false;
|
||||
} else {
|
||||
// Handle unexpected response format
|
||||
showToast('Unexpected response format', 'error');
|
||||
errorMessage.textContent = "Unexpected response format";
|
||||
errorAlert.style.display = 'block';
|
||||
|
||||
// Restore button state
|
||||
createButton.innerHTML = originalButtonText;
|
||||
createButton.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle network or other errors
|
||||
showToast('Network error: ' + error.message, 'error');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
errorMessage.textContent = "Network error: " + error.message;
|
||||
errorAlert.style.display = 'block';
|
||||
|
||||
// Restore button state
|
||||
createButton.innerHTML = originalButtonText;
|
||||
createButton.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,600 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Create Agent Group</title>
|
||||
{{template "views/partials/header"}}
|
||||
<script src="/public/js/wizard.js"></script>
|
||||
<link rel="stylesheet" href="/public/css/wizard.css">
|
||||
<script src="/public/js/connector-templates.js"></script>
|
||||
<script src="/public/js/agent-form.js"></script>
|
||||
<style>
|
||||
.agent-profile {
|
||||
border: 1px solid var(--medium-bg);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
background-color: var(--lighter-bg);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.agent-profile:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.agent-profile h3 {
|
||||
color: var(--primary);
|
||||
text-shadow: var(--neon-glow);
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid var(--medium-bg);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.agent-profile .description {
|
||||
color: var(--text);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.agent-profile .system-prompt {
|
||||
background-color: var(--darker-bg);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 0.85rem;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 10px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.agent-profile.selected {
|
||||
border: 2px solid var(--primary);
|
||||
background-color: rgba(var(--primary-rgb), 0.1);
|
||||
}
|
||||
.agent-profile .select-checkbox {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.page-section {
|
||||
display: none;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
.page-section.active {
|
||||
display: block;
|
||||
}
|
||||
.progress-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.progress-step:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: -30px;
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background-color: var(--medium-bg);
|
||||
}
|
||||
.progress-step.active:not(:last-child)::after {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
.step-circle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--medium-bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text);
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.progress-step.active .step-circle {
|
||||
background-color: var(--primary);
|
||||
box-shadow: 0 0 10px var(--primary);
|
||||
}
|
||||
.step-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted-text);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.progress-step.active .step-label {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
.prompt-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.prompt-container textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--lighter-bg);
|
||||
border: 1px solid var(--medium-bg);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
resize: vertical;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.select-all-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.loader {
|
||||
display: none;
|
||||
text-align: center;
|
||||
margin: 40px 0;
|
||||
}
|
||||
.loader i {
|
||||
color: var(--primary);
|
||||
font-size: 2rem;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
/* Make form elements auto in group mode */
|
||||
#wizard-container .form-section {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.info-message {
|
||||
background-color: rgba(var(--primary-rgb), 0.1);
|
||||
border-left: 4px solid var(--primary);
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.info-message i {
|
||||
font-size: 1.5rem;
|
||||
color: var(--primary);
|
||||
margin-right: 15px;
|
||||
}
|
||||
.info-message-content {
|
||||
flex: 1;
|
||||
}
|
||||
.info-message-content h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
color: var(--primary);
|
||||
}
|
||||
.info-message-content p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="container">
|
||||
<div class="section-box">
|
||||
<h1>Create Agent Group</h1>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-step active" data-step="1">
|
||||
<div class="step-circle">1</div>
|
||||
<div class="step-label">Generate Profiles</div>
|
||||
</div>
|
||||
<div class="progress-step" data-step="2">
|
||||
<div class="step-circle">2</div>
|
||||
<div class="step-label">Review & Select</div>
|
||||
</div>
|
||||
<div class="progress-step" data-step="3">
|
||||
<div class="step-circle">3</div>
|
||||
<div class="step-label">Configure Settings</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Generate Profiles -->
|
||||
<div id="step1" class="page-section active">
|
||||
<h2>Generate Agent Profiles</h2>
|
||||
<p>Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.</p>
|
||||
|
||||
<div class="prompt-container">
|
||||
<textarea id="group-description" placeholder="Example: Create a team of agents for a software development project including a project manager, developer, tester, and designer. They should collaborate to build web applications."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button type="button" id="generate-profiles-btn" class="action-btn">
|
||||
<i class="fas fa-magic"></i> Generate Profiles
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loader -->
|
||||
<div id="loader" class="loader">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<p>Generating agent profiles...</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Review & Select Profiles -->
|
||||
<div id="step2" class="page-section">
|
||||
<h2>Review & Select Agent Profiles</h2>
|
||||
<p>Select the agents you want to create. You can customize their details before creation.</p>
|
||||
|
||||
<div class="select-all-container">
|
||||
<label for="select-all" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" id="select-all">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Select All
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="agent-profiles-container">
|
||||
<!-- Agent profiles will be generated here -->
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button type="button" id="back-to-step1-btn" class="nav-btn">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</button>
|
||||
<button type="button" id="to-step3-btn" class="action-btn">
|
||||
Continue <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Common Settings -->
|
||||
<div id="step3" class="page-section">
|
||||
<h2>Configure Common Settings</h2>
|
||||
<p>Configure common settings for all selected agents. These settings will be applied to each agent.</p>
|
||||
|
||||
<form id="group-settings-form">
|
||||
<!-- Informative message about profile data -->
|
||||
<div class="info-message">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<div class="info-message-content">
|
||||
<h4>Basic Information from Profiles</h4>
|
||||
<p>The name, description, and system prompt for each agent will be taken from the profiles you selected in the previous step.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Use the existing agent-form partial -->
|
||||
<div id="group-agent-form">
|
||||
{{template "views/partials/agent-form" . }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button type="button" id="back-to-step2-btn" class="nav-btn">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</button>
|
||||
<button type="button" id="create-group-btn" class="action-btn" data-original-text="<i class='fas fa-users'></i> Create Agent Group">
|
||||
<i class="fas fa-users"></i> Create Agent Group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Messages Container -->
|
||||
<div id="response-container">
|
||||
<!-- Alert messages will be shown here -->
|
||||
<div id="success-alert" class="alert alert-success" style="display: none;">
|
||||
Agents created successfully! Redirecting to agent list...
|
||||
</div>
|
||||
|
||||
<div id="error-alert" class="alert alert-error" style="display: none;">
|
||||
<span id="error-message">Error creating agents.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast notification container -->
|
||||
<div id="toast" class="toast">
|
||||
<span id="toast-message"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
|
||||
// Store generated agent profiles
|
||||
let agentProfiles = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize the form components
|
||||
initAgentFormCommon({
|
||||
actions: actions,
|
||||
connectors: connectors,
|
||||
promptBlocks: promptBlocks
|
||||
});
|
||||
|
||||
// Hide the Basic Information section
|
||||
const basicSection = document.getElementById('basic-section');
|
||||
if (basicSection) {
|
||||
basicSection.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update the wizard navigation items to skip Basic Information
|
||||
const basicNavItem = document.querySelector('.wizard-nav-item[data-target="basic-section"]');
|
||||
if (basicNavItem) {
|
||||
basicNavItem.style.display = 'none';
|
||||
}
|
||||
|
||||
// Make sure Connectors section is active by default
|
||||
const connectorsSection = document.getElementById('connectors-section');
|
||||
if (connectorsSection) {
|
||||
document.querySelectorAll('.form-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
});
|
||||
connectorsSection.classList.add('active');
|
||||
}
|
||||
|
||||
// Update the active nav item
|
||||
const connectorsNavItem = document.querySelector('.wizard-nav-item[data-target="connectors-section"]');
|
||||
if (connectorsNavItem) {
|
||||
document.querySelectorAll('.wizard-nav-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
connectorsNavItem.classList.add('active');
|
||||
}
|
||||
|
||||
// Update the current step label
|
||||
const currentStepLabel = document.getElementById('currentStepLabel');
|
||||
if (currentStepLabel) {
|
||||
currentStepLabel.textContent = 'Connectors';
|
||||
}
|
||||
|
||||
// Navigation between steps
|
||||
const goToStep = (stepNumber) => {
|
||||
// Hide all steps
|
||||
document.querySelectorAll('.page-section').forEach(section => {
|
||||
section.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show the target step
|
||||
document.getElementById(`step${stepNumber}`).classList.add('active');
|
||||
|
||||
// Update progress bar
|
||||
document.querySelectorAll('.progress-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
if (parseInt(step.dataset.step) <= stepNumber) {
|
||||
step.classList.add('active');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Step 1: Generate Profiles
|
||||
document.getElementById('generate-profiles-btn').addEventListener('click', function() {
|
||||
const description = document.getElementById('group-description').value.trim();
|
||||
|
||||
if (!description) {
|
||||
showToast('Please enter a description for your agent group', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loader
|
||||
document.getElementById('loader').style.display = 'block';
|
||||
document.getElementById('step1').style.display = 'none';
|
||||
|
||||
// Send request to generate profiles
|
||||
fetch('/api/agent/group/generateProfiles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ description: description })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Hide loader
|
||||
document.getElementById('loader').style.display = 'none';
|
||||
|
||||
agentProfiles = Array.isArray(data) ? data : [];
|
||||
|
||||
if (agentProfiles.length === 0) {
|
||||
showToast('No agent profiles were generated. Please try again with a more detailed description.', 'error');
|
||||
document.getElementById('step1').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render agent profiles
|
||||
renderAgentProfiles();
|
||||
|
||||
// Go to step 2
|
||||
goToStep(2);
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('loader').style.display = 'none';
|
||||
document.getElementById('step1').style.display = 'block';
|
||||
showToast('Error generating profiles: ' + error.message, 'error');
|
||||
console.error('Error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Render agent profiles in step 2
|
||||
function renderAgentProfiles() {
|
||||
const container = document.getElementById('agent-profiles-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
agentProfiles.forEach((profile, index) => {
|
||||
const profileElement = document.createElement('div');
|
||||
profileElement.className = 'agent-profile';
|
||||
profileElement.dataset.index = index;
|
||||
profileElement.innerHTML = `
|
||||
<label class="select-checkbox checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" class="profile-checkbox" checked>
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
</label>
|
||||
<h3>${profile.name}</h3>
|
||||
<div class="description">${profile.description}</div>
|
||||
<div class="system-prompt">${profile.system_prompt}</div>
|
||||
`;
|
||||
|
||||
profileElement.querySelector('.profile-checkbox').addEventListener('change', function() {
|
||||
profileElement.classList.toggle('selected', this.checked);
|
||||
updateSelectAllCheckbox();
|
||||
});
|
||||
|
||||
// Initially set as selected
|
||||
profileElement.classList.add('selected');
|
||||
|
||||
container.appendChild(profileElement);
|
||||
});
|
||||
}
|
||||
|
||||
// Select all checkbox functionality
|
||||
document.getElementById('select-all').addEventListener('change', function() {
|
||||
const isChecked = this.checked;
|
||||
document.querySelectorAll('.profile-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.closest('.agent-profile').classList.toggle('selected', isChecked);
|
||||
});
|
||||
});
|
||||
|
||||
function updateSelectAllCheckbox() {
|
||||
const checkboxes = document.querySelectorAll('.profile-checkbox');
|
||||
const selectAllCheckbox = document.getElementById('select-all');
|
||||
const allChecked = Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||
const someChecked = Array.from(checkboxes).some(checkbox => checkbox.checked);
|
||||
|
||||
selectAllCheckbox.checked = allChecked;
|
||||
selectAllCheckbox.indeterminate = !allChecked && someChecked;
|
||||
}
|
||||
|
||||
// Navigation buttons
|
||||
document.getElementById('back-to-step1-btn').addEventListener('click', () => goToStep(1));
|
||||
document.getElementById('to-step3-btn').addEventListener('click', () => {
|
||||
// Check if at least one profile is selected
|
||||
const selectedProfiles = document.querySelectorAll('.profile-checkbox:checked');
|
||||
if (selectedProfiles.length === 0) {
|
||||
showToast('Please select at least one agent profile', 'error');
|
||||
return;
|
||||
}
|
||||
goToStep(3);
|
||||
});
|
||||
document.getElementById('back-to-step2-btn').addEventListener('click', () => goToStep(2));
|
||||
|
||||
// Create group button
|
||||
document.getElementById('create-group-btn').addEventListener('click', function() {
|
||||
// Get selected profiles
|
||||
const selectedProfileIndices = Array.from(document.querySelectorAll('.profile-checkbox:checked'))
|
||||
.map(checkbox => parseInt(checkbox.closest('.agent-profile').dataset.index));
|
||||
|
||||
if (selectedProfileIndices.length === 0) {
|
||||
showToast('Please select at least one agent profile', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProfiles = selectedProfileIndices.map(index => agentProfiles[index]);
|
||||
|
||||
// Process form data for common settings
|
||||
const formData = new FormData(document.getElementById('group-settings-form'));
|
||||
const commonSettings = AgentFormUtils.processFormData(formData);
|
||||
|
||||
// Process special fields
|
||||
commonSettings.connectors = AgentFormUtils.processConnectors(this);
|
||||
if (commonSettings.connectors === null) return; // Validation failed
|
||||
|
||||
commonSettings.mcp_servers = AgentFormUtils.processMCPServers();
|
||||
|
||||
commonSettings.actions = AgentFormUtils.processActions(this);
|
||||
if (commonSettings.actions === null) return; // Validation failed
|
||||
|
||||
commonSettings.promptblocks = AgentFormUtils.processPromptBlocks(this);
|
||||
if (commonSettings.promptblocks === null) return; // Validation failed
|
||||
|
||||
// Show loading state
|
||||
const createButton = document.getElementById('create-group-btn');
|
||||
const originalButtonText = createButton.innerHTML;
|
||||
createButton.setAttribute('data-original-text', originalButtonText);
|
||||
createButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
|
||||
createButton.disabled = true;
|
||||
|
||||
// Create payload
|
||||
const payload = {
|
||||
agents: selectedProfiles,
|
||||
agent_config: commonSettings
|
||||
};
|
||||
|
||||
// Send request to create agents
|
||||
fetch('/api/agent/group/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const successAlert = document.getElementById('success-alert');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
// Hide both alerts initially
|
||||
successAlert.style.display = 'none';
|
||||
errorAlert.style.display = 'none';
|
||||
|
||||
if (data.status === "ok") {
|
||||
// Show success toast
|
||||
showToast(`${selectedProfiles.length} agent(s) created successfully!`, 'success');
|
||||
|
||||
// Show success message
|
||||
successAlert.style.display = 'block';
|
||||
|
||||
// Redirect to agent list page after a delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/agents';
|
||||
}, 2000);
|
||||
} else if (data.error) {
|
||||
// Show error toast
|
||||
showToast('Error: ' + data.error, 'error');
|
||||
|
||||
// Show error message
|
||||
errorMessage.textContent = data.error;
|
||||
errorAlert.style.display = 'block';
|
||||
|
||||
// Restore button state
|
||||
createButton.innerHTML = originalButtonText;
|
||||
createButton.disabled = false;
|
||||
} else {
|
||||
// Handle unexpected response format
|
||||
showToast('Unexpected response format', 'error');
|
||||
errorMessage.textContent = "Unexpected response format";
|
||||
errorAlert.style.display = 'block';
|
||||
|
||||
// Restore button state
|
||||
createButton.innerHTML = originalButtonText;
|
||||
createButton.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle network or other errors
|
||||
showToast('Network error: ' + error.message, 'error');
|
||||
const errorAlert = document.getElementById('error-alert');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
errorMessage.textContent = "Network error: " + error.message;
|
||||
errorAlert.style.display = 'block';
|
||||
|
||||
// Restore button state
|
||||
createButton.innerHTML = originalButtonText;
|
||||
createButton.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Toast notification function - assuming this exists in your global scope
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,224 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Smart Assistant Dashboard</title>
|
||||
{{template "views/partials/header"}}
|
||||
<style>
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 40px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
filter: drop-shadow(0 0 15px rgba(94, 0, 255, 0.6));
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.image-container:hover img {
|
||||
transform: scale(1.05);
|
||||
filter: drop-shadow(0 0 25px rgba(0, 255, 149, 0.8));
|
||||
}
|
||||
|
||||
.image-container::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--primary), var(--secondary), var(--tertiary), transparent);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: 2px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--primary);
|
||||
text-shadow: var(--neon-glow);
|
||||
animation: gentlePulse 3s infinite;
|
||||
}
|
||||
|
||||
/* Gentle pulse animation for the title */
|
||||
@keyframes gentlePulse {
|
||||
0% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
|
||||
50% { text-shadow: 0 0 12px var(--primary), 0 0 20px var(--primary); }
|
||||
100% { text-shadow: 0 0 7px var(--primary), 0 0 10px var(--primary); }
|
||||
}
|
||||
|
||||
/* Subtle glitch effect for hover */
|
||||
@keyframes subtleGlitch {
|
||||
0% { transform: translateX(-50%); }
|
||||
20% { transform: translateX(-50%) translate(-1px, 1px); }
|
||||
40% { transform: translateX(-50%) translate(-1px, -1px); }
|
||||
60% { transform: translateX(-50%) translate(1px, 1px); }
|
||||
80% { transform: translateX(-50%) translate(1px, -1px); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin: 20px 0 40px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(17, 17, 17, 0.7);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(94, 0, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--primary), var(--secondary));
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.stat-count {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
text-shadow: var(--neon-glow);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #cccccc;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Badge positioning */
|
||||
.card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="container">
|
||||
|
||||
|
||||
<div class="image-container">
|
||||
<img src="/public/logo_1.png" width="250" alt="Company Logo">
|
||||
</div>
|
||||
|
||||
<h1 class="dashboard-title">LocalAgent</h1>
|
||||
|
||||
<!-- Simple stats display -->
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-count">{{.Actions}}</div>
|
||||
<div class="stat-label">Available Actions</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-count">{{.Connectors}}</div>
|
||||
<div class="stat-label">Available Connectors</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-count">{{ .AgentCount }}</div>
|
||||
<div class="stat-label">Agents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cards-container">
|
||||
<!-- Card for Agent List Page -->
|
||||
<a href="/agents" class="card-link">
|
||||
<div class="card">
|
||||
<h2><i class="fas fa-robot"></i> Agent List</h2>
|
||||
<p>View and manage your list of agents, including detailed profiles and statistics.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Card for Create Agent -->
|
||||
<a href="/create" class="card-link">
|
||||
<div class="card">
|
||||
<h2><i class="fas fa-plus-circle"></i> Create Agent</h2>
|
||||
<p>Create a new intelligent agent with custom behaviors, connectors, and actions.</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Additional Cards for Future Features -->
|
||||
<a href="#" class="card-link">
|
||||
<div class="card">
|
||||
<h2><i class="fas fa-chart-line"></i> Analytics</h2>
|
||||
<p>View performance metrics and insights from your agent operations.</p>
|
||||
<span class="badge badge-secondary">Coming Soon</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="#" class="card-link">
|
||||
<div class="card">
|
||||
<h2><i class="fas fa-cogs"></i> Settings</h2>
|
||||
<p>Configure system preferences and global settings for all agents.</p>
|
||||
<span class="badge badge-secondary">Coming Soon</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast notification container -->
|
||||
<div id="toast" class="toast">
|
||||
<span id="toast-message"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add the controlled glitch effect to dashboard title
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const title = document.querySelector('.dashboard-title');
|
||||
|
||||
title.addEventListener('mouseover', function() {
|
||||
// Use the more subtle glitch animation
|
||||
this.style.animation = 'subtleGlitch 0.5s infinite';
|
||||
});
|
||||
|
||||
title.addEventListener('mouseout', function() {
|
||||
// Return to gentle pulse animation
|
||||
this.style.animation = 'gentlePulse 3s infinite';
|
||||
});
|
||||
|
||||
// Welcome toast notification
|
||||
setTimeout(() => {
|
||||
showToast('Welcome to Smart Assistant Dashboard', 'success');
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,215 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "views/partials/header" .}}
|
||||
|
||||
<body class="bg-gradient-to-br from-gray-900 to-gray-950 text-gray-200">
|
||||
<div class="flex flex-col min-h-screen">
|
||||
|
||||
{{template "views/partials/menu" .}}
|
||||
|
||||
<div class="container mx-auto px-4 py-8 flex-grow flex items-center justify-center">
|
||||
<!-- Auth Card -->
|
||||
<div class="max-w-md w-full bg-gray-800/90 border border-gray-700/50 rounded-xl overflow-hidden shadow-xl">
|
||||
<div class="animation-container">
|
||||
<div class="text-overlay">
|
||||
<!-- <i class="fas fa-circle-nodes text-5xl text-blue-400 mb-2"></i> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-8">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-bold text-white">
|
||||
<span class="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-indigo-400">
|
||||
Authorization Required
|
||||
</span>
|
||||
</h2>
|
||||
<p class="text-gray-400 mt-2">Please enter your access token to continue</p>
|
||||
</div>
|
||||
|
||||
<form id="login-form" class="space-y-6" onsubmit="login(); return false;">
|
||||
<div>
|
||||
<label for="token" class="block text-sm font-medium text-gray-300 mb-2">Access Token</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-key text-gray-500"></i>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="token"
|
||||
name="token"
|
||||
placeholder="Enter your token"
|
||||
class="bg-gray-700/50 border border-gray-600 text-white placeholder-gray-400 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2.5"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="group w-full flex items-center justify-center bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white py-3 px-6 rounded-lg transition duration-300 ease-in-out transform hover:scale-[1.02] hover:shadow-lg font-medium"
|
||||
>
|
||||
<i class="fas fa-sign-in-alt mr-2"></i>
|
||||
<span>Login</span>
|
||||
<i class="fas fa-arrow-right opacity-0 group-hover:opacity-100 group-hover:translate-x-2 ml-2 transition-all duration-300"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 pt-6 border-t border-gray-700/50 text-center text-sm text-gray-400">
|
||||
<div class="flex items-center justify-center mb-2">
|
||||
<i class="fas fa-shield-alt mr-2 text-blue-400"></i>
|
||||
<span>Instance is token protected</span>
|
||||
</div>
|
||||
<p>Current time (UTC): <span id="current-time">{{.CurrentDate}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function login() {
|
||||
const token = document.getElementById('token').value;
|
||||
if (!token.trim()) {
|
||||
// Show error with fading effect
|
||||
const form = document.getElementById('login-form');
|
||||
const errorMsg = document.createElement('div');
|
||||
errorMsg.className = 'p-3 mt-4 bg-red-900/50 text-red-200 rounded-lg border border-red-700/50 text-sm flex items-center';
|
||||
errorMsg.innerHTML = '<i class="fas fa-exclamation-circle mr-2"></i> Please enter a valid token';
|
||||
|
||||
// Remove any existing error message
|
||||
const existingError = form.querySelector('.bg-red-900/50');
|
||||
if (existingError) form.removeChild(existingError);
|
||||
|
||||
// Add new error message with animation
|
||||
form.appendChild(errorMsg);
|
||||
setTimeout(() => {
|
||||
errorMsg.style.opacity = '0';
|
||||
errorMsg.style.transition = 'opacity 0.5s ease';
|
||||
setTimeout(() => errorMsg.remove(), 500);
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + (24*60*60*1000));
|
||||
document.cookie = `token=${token}; expires=${date.toGMTString()}; path=/`;
|
||||
|
||||
// Show loading state
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
const originalContent = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Authenticating...';
|
||||
button.classList.add('bg-gray-600');
|
||||
|
||||
// Reload after short delay to show loading state
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 800);
|
||||
}
|
||||
|
||||
// Update current time
|
||||
function updateCurrentTime() {
|
||||
const timeElement = document.getElementById('current-time');
|
||||
if (timeElement) {
|
||||
const now = new Date();
|
||||
const year = now.getUTCFullYear();
|
||||
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getUTCDate()).padStart(2, '0');
|
||||
const hours = String(now.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
|
||||
timeElement.textContent = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize current time and update it every second
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
|
||||
// Add subtle particle animation to the background
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const animContainer = document.querySelector('.animation-container');
|
||||
if (animContainer) {
|
||||
const canvas = document.createElement('canvas');
|
||||
animContainer.appendChild(canvas);
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = animContainer.offsetWidth;
|
||||
canvas.height = animContainer.offsetHeight;
|
||||
|
||||
// Create particles
|
||||
const particles = [];
|
||||
const particleCount = 30;
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
radius: Math.random() * 3 + 1,
|
||||
color: `rgba(${Math.random() * 50 + 50}, ${Math.random() * 100 + 100}, ${Math.random() * 155 + 100}, ${Math.random() * 0.4 + 0.1})`,
|
||||
speedX: Math.random() * 0.5 - 0.25,
|
||||
speedY: Math.random() * 0.5 - 0.25
|
||||
});
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
particles.forEach(particle => {
|
||||
particle.x += particle.speedX;
|
||||
particle.y += particle.speedY;
|
||||
|
||||
// Bounce off edges
|
||||
if (particle.x < 0 || particle.x > canvas.width) {
|
||||
particle.speedX = -particle.speedX;
|
||||
}
|
||||
|
||||
if (particle.y < 0 || particle.y > canvas.height) {
|
||||
particle.speedY = -particle.speedY;
|
||||
}
|
||||
|
||||
// Draw particle
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// Connect nearby particles with lines
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
for (let j = i + 1; j < particles.length; j++) {
|
||||
const dx = particles[i].x - particles[j].x;
|
||||
const dy = particles[i].y - particles[j].y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < 100) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(particles[i].x, particles[i].y);
|
||||
ctx.lineTo(particles[j].x, particles[j].y);
|
||||
ctx.strokeStyle = `rgba(100, 150, 255, ${0.1 * (1 - distance / 100)})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start animation
|
||||
animate();
|
||||
|
||||
// Resize handling
|
||||
window.addEventListener('resize', () => {
|
||||
canvas.width = animContainer.offsetWidth;
|
||||
canvas.height = animContainer.offsetHeight;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,307 +0,0 @@
|
||||
<div class="agent-form-container">
|
||||
<!-- Wizard Sidebar -->
|
||||
<div class="wizard-sidebar">
|
||||
<ul class="wizard-nav">
|
||||
<li class="wizard-nav-item active" data-target="basic-section">
|
||||
<i class="fas fa-info-circle"></i> Basic Information
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="connectors-section">
|
||||
<i class="fas fa-plug"></i> Connectors
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="actions-section">
|
||||
<i class="fas fa-bolt"></i> Actions
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="mcp-section">
|
||||
<i class="fas fa-server"></i> MCP Servers
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="memory-section">
|
||||
<i class="fas fa-memory"></i> Memory Settings
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="model-section">
|
||||
<i class="fas fa-robot"></i> Model Settings
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="prompts-section">
|
||||
<i class="fas fa-comment-alt"></i> Prompts & Goals
|
||||
</li>
|
||||
<li class="wizard-nav-item" data-target="advanced-section">
|
||||
<i class="fas fa-cogs"></i> Advanced Settings
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Form Content Area -->
|
||||
<div class="form-content-area">
|
||||
<!-- Basic Information Section -->
|
||||
<div class="form-section active" id="basic-section">
|
||||
<h3 class="section-title">Basic Information</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="name">Name</label>
|
||||
{{ if .Name }}
|
||||
<input type="text" name="name" id="name" placeholder="Name" value="{{.Name}}" readonly >
|
||||
{{ else }}
|
||||
<input type="text" name="name" id="name" placeholder="Name">
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" id="description" placeholder="Description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="identity_guidance">Identity Guidance</label>
|
||||
<textarea name="identity_guidance" id="identity_guidance" placeholder="Identity Guidance"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="random_identity" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="random_identity" id="random_identity">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Random Identity
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="hud" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="hud" id="hud">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
HUD
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connectors Section -->
|
||||
<div class="form-section" id="connectors-section">
|
||||
<h3 class="section-title">Connectors</h3>
|
||||
|
||||
<div id="connectorsSection">
|
||||
<!-- Connectors will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button type="button" id="addConnectorButton" class="action-btn">
|
||||
<i class="fas fa-plus-circle"></i> Add Connector
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="form-section" id="actions-section">
|
||||
<h3 class="section-title">Actions</h3>
|
||||
|
||||
<div class="mb-4" id="action_box">
|
||||
<!-- Actions will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="action_button" type="button" class="action-btn">
|
||||
<i class="fas fa-plus-circle"></i> Add Action
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP Servers Section -->
|
||||
<div class="form-section" id="mcp-section">
|
||||
<h3 class="section-title">MCP Servers</h3>
|
||||
|
||||
<div id="mcpSection">
|
||||
<!-- MCP servers will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button type="button" id="addMCPButton" class="action-btn">
|
||||
<i class="fas fa-plus-circle"></i> Add MCP Server
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Settings Section -->
|
||||
<div class="form-section" id="memory-section">
|
||||
<h3 class="section-title">Memory Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="enable_kb" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="enable_kb" id="enable_kb">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Enable Knowledge Base
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="kb_results">Knowledge Base Results</label>
|
||||
<input type="number" name="kb_results" id="kb_results" placeholder="3">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="long_term_memory" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="long_term_memory" id="long_term_memory">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Long Term Memory
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="summary_long_term_memory" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="summary_long_term_memory" id="summary_long_term_memory">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Long Term Memory (Summarize!)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Settings Section -->
|
||||
<div class="form-section" id="model-section">
|
||||
<h3 class="section-title">Model Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="model">Model </label>
|
||||
<input type="text" name="model" id="model" placeholder="Model name">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="multimodal_model">Multimodal Model </label>
|
||||
<input type="text" name="multimodal_model" id="multimodal_model" placeholder="Model name">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="api_url">API URL </label>
|
||||
<input type="text" name="api_url" id="api_url" placeholder="API URL">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="api_key">API Key </label>
|
||||
<input type="text" name="api_key" id="api_key" placeholder="API Key">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="local_rag_url">LocalRAG API URL </label>
|
||||
<input type="text" name="local_rag_url" id="local_rag_url" placeholder="LocalRAG API URL">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="local_rag_api_key">LocalRAG API Key </label>
|
||||
<input type="text" name="local_rag_api_key" id="local_rag_api_key" placeholder="LocalRAG API Key">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="enable_reasoning" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="enable_reasoning" id="enable_reasoning">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Enable Reasoning
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompts & Goals Section -->
|
||||
<div class="form-section" id="prompts-section">
|
||||
<h3 class="section-title">Prompts & Goals</h3>
|
||||
|
||||
<div class="mb-4" id="dynamic_box">
|
||||
<!-- Dynamic prompts will be added here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="dynamic_button" type="button" class="action-btn">
|
||||
<i class="fas fa-plus-circle"></i> Add Dynamic Prompt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="system_prompt">System Prompt</label>
|
||||
<textarea name="system_prompt" id="system_prompt" placeholder="System prompt"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="permanent_goal">Permanent Goal</label>
|
||||
<textarea name="permanent_goal" id="permanent_goal" placeholder="Permanent goal"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings Section -->
|
||||
<div class="form-section" id="advanced-section">
|
||||
<h3 class="section-title">Advanced Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="standalone_job" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="standalone_job" id="standalone_job">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Standalone Job
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="initiate_conversations" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="initiate_conversations" id="initiate_conversations">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Initiate Conversations
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="enable_planning" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="enable_planning" id="enable_planning">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Enable Planning
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="can_stop_itself" class="checkbox-label">
|
||||
<span class="checkbox-custom">
|
||||
<input type="checkbox" name="can_stop_itself" id="can_stop_itself">
|
||||
<span class="checkmark"></span>
|
||||
</span>
|
||||
Can Stop Itself
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="periodic_runs">Periodic Runs</label>
|
||||
<input type="text" name="periodic_runs" id="periodic_runs" placeholder="Periodic Runs">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add navigation controls at the bottom -->
|
||||
<div class="wizard-controls">
|
||||
<div class="wizard-controls-left">
|
||||
<button type="button" id="prevSection" class="nav-btn">
|
||||
<i class="fas fa-arrow-left"></i> Previous
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="wizard-controls-center">
|
||||
<div class="progress-dots" id="progressDots">
|
||||
<!-- Dots will be added dynamically via JS -->
|
||||
</div>
|
||||
<div class="progress-indicator">
|
||||
<span id="currentStepLabel">Basic Information</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-controls-right">
|
||||
<button type="button" id="nextSection" class="nav-btn">
|
||||
Next <i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Permanent+Marker&display=swap" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org"></script>
|
||||
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
|
||||
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
|
||||
<link rel="stylesheet" href="/public/css/styles.css">
|
||||
<script src="/public/js/common.js"></script>
|
||||
@@ -1,105 +0,0 @@
|
||||
<nav class="relative z-10 w-full" style="background-color: var(--darker-bg); border-bottom: 1px solid var(--medium-bg);">
|
||||
<div class="px-6 sm:px-8 lg:px-10">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<div class="flex items-center">
|
||||
<!-- Logo container -->
|
||||
<div class="flex-shrink-0">
|
||||
<!-- Logo with glow effect -->
|
||||
<a href="/" class="flex items-center group">
|
||||
<div class="relative">
|
||||
<img src="/public/logo_1.png" alt="Logo" class="h-10 w-auto mr-4 transition-transform duration-300 group-hover:scale-105"
|
||||
style="filter: drop-shadow(0 0 5px var(--primary));">
|
||||
<!-- Animated scan line on hover -->
|
||||
<div class="absolute inset-0 overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-var(--primary) to-transparent opacity-30"
|
||||
style="height: 10px; animation: scanline 1.5s linear infinite;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xl font-bold transition-colors duration-300"
|
||||
style="color: var(--primary); text-shadow: var(--neon-glow);">LocalAgent</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden md:block ml-10">
|
||||
<div class="flex space-x-4">
|
||||
<a href="/" class="px-3 py-2 rounded-md text-lg font-medium text-white hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||
<i class="fas fa-home mr-2"></i> Home
|
||||
<!-- Underline animation -->
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style="background: linear-gradient(90deg, var(--primary), var(--secondary));"></span>
|
||||
</a>
|
||||
<a href="/agents" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||
<i class="fas fa-users mr-2"></i> Agent List
|
||||
<!-- Underline animation -->
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style="background: linear-gradient(90deg, var(--secondary), var(--tertiary));"></span>
|
||||
</a>
|
||||
<a href="/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||
<i class="fas fa-bolt mr-2"></i> Actions Playground
|
||||
<!-- Underline animation -->
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
|
||||
</a>
|
||||
<a href="/group-create" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||
<i class="fas fa-users-cog mr-2"></i> Create Agent Group
|
||||
<!-- Underline animation -->
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style="background: linear-gradient(90deg, var(--secondary), var(--primary));"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Status badge -->
|
||||
<div class="hidden md:flex items-center">
|
||||
<span class="flex items-center text-sm">
|
||||
<span class="w-2 h-2 rounded-full mr-2"
|
||||
style="background-color: var(--primary); box-shadow: 0 0 5px var(--primary); animation: pulse 2s infinite;"></span>
|
||||
<span>State: <span style="color: var(--secondary); text-shadow: var(--pink-glow);">active</span></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:hidden flex items-center">
|
||||
<button class="text-gray-400 hover:text-white focus:outline-none focus:text-white transition duration-300"
|
||||
style="text-shadow: var(--neon-glow);"
|
||||
onclick="toggleMobileMenu()">
|
||||
<i class="fas fa-bars fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu, toggle based on menu state -->
|
||||
<div id="mobile-menu" class="md:hidden hidden"
|
||||
style="background-color: var(--darker-bg); border-top: 1px solid var(--medium-bg);">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<a href="/" class="block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-gray-800 transition duration-300"
|
||||
style="border-left: 3px solid var(--primary);">
|
||||
<i class="fas fa-home mr-2"></i> Home
|
||||
</a>
|
||||
<a href="/agents" class="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:bg-gray-800 transition duration-300"
|
||||
style="border-left: 3px solid var(--secondary);">
|
||||
<i class="fas fa-users mr-2"></i> Agent List
|
||||
</a>
|
||||
<a href="/actions-playground" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||
<i class="fas fa-bolt mr-2"></i> Actions Playground
|
||||
<!-- Underline animation -->
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style="background: linear-gradient(90deg, var(--tertiary), var(--primary));"></span>
|
||||
</a>
|
||||
<a href="/group-create" class="px-3 py-2 rounded-md text-lg font-medium text-gray-400 hover:bg-gray-800 transition duration-300 relative overflow-hidden group">
|
||||
<i class="fas fa-users-cog mr-2"></i> Create Agent Group
|
||||
<!-- Underline animation -->
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 group-hover:w-full transition-all duration-300"
|
||||
style="background: linear-gradient(90deg, var(--secondary), var(--primary));"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<br>
|
||||
|
||||
<script>
|
||||
function toggleMobileMenu() {
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
mobileMenu.classList.toggle('hidden');
|
||||
}
|
||||
</script>
|
||||
@@ -1,462 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Agent settings {{.Name}}</title>
|
||||
{{template "views/partials/header"}}
|
||||
<script src="/public/js/wizard.js"></script>
|
||||
<link rel="stylesheet" href="/public/css/wizard.css">
|
||||
<script src="/public/js/connector-templates.js"></script>
|
||||
<script src="/public/js/agent-form.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
|
||||
<!-- Toast notification container -->
|
||||
<div id="toast" class="toast">
|
||||
<span id="toast-message"></span>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-3xl md:text-5xl font-bold">Agent settings - {{.Name}}</h1>
|
||||
</header>
|
||||
|
||||
<div class="max-w-4xl mx-auto">
|
||||
|
||||
<!-- Agent Configuration Form Section -->
|
||||
<div class="section-box">
|
||||
<h2>Edit Agent Configuration</h2>
|
||||
<form id="edit-agent-form">
|
||||
<input type="hidden" name="name" id="name" value="{{.Name}}">
|
||||
|
||||
{{template "views/partials/agent-form" .}}
|
||||
|
||||
<button type="submit" id="update-button" class="action-btn" data-original-text="<i class='fas fa-save'></i> Update Agent">
|
||||
<i class="fas fa-save"></i> Update Agent
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Agent Control</h2>
|
||||
<div class="button-container">
|
||||
<button
|
||||
class="action-btn toggle-btn"
|
||||
data-agent="{{.Name}}"
|
||||
data-active="{{.Status}}">
|
||||
{{if .Status}}
|
||||
<i class="fas fa-pause"></i> Pause Agent
|
||||
{{else}}
|
||||
<i class="fas fa-play"></i> Start Agent
|
||||
{{end}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Export Data</h2>
|
||||
<p class="mb-4">Export your agent configuration for backup or transfer.</p>
|
||||
<button
|
||||
class="action-btn"
|
||||
onclick="window.location.href='/settings/export/{{.Name}}'">
|
||||
<i class="fas fa-file-export"></i> Export Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Danger Zone</h2>
|
||||
<p class="mb-4">Permanently delete this agent and all associated data. This action cannot be undone.</p>
|
||||
<button
|
||||
class="action-btn"
|
||||
style="background: linear-gradient(135deg, #ff4545, var(--secondary)); color: white;"
|
||||
hx-delete="/delete/{{.Name}}"
|
||||
hx-swap="none"
|
||||
data-action="delete"
|
||||
data-agent="{{.Name}}">
|
||||
<i class="fas fa-trash-alt"></i> Delete Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="user-info">
|
||||
<span>Agent: {{.Name}}</span>
|
||||
<span class="timestamp">Last modified: <span id="current-date"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const promptBlocks = `{{ range .PromptBlocks }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
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();
|
||||
|
||||
// Add event listener for delete button
|
||||
document.querySelectorAll('[data-action="delete"]').forEach(button => {
|
||||
button.addEventListener('htmx:afterRequest', function(event) {
|
||||
handleActionResponse(event, this);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle toggle button
|
||||
const toggleButton = document.querySelector('.toggle-btn');
|
||||
if (toggleButton) {
|
||||
toggleButton.addEventListener('click', function() {
|
||||
const agent = this.getAttribute('data-agent');
|
||||
const isActive = this.getAttribute('data-active') === "true";
|
||||
const endpoint = isActive ? `/pause/${agent}` : `/start/${agent}`;
|
||||
|
||||
// Add animation
|
||||
this.style.animation = 'pulse 0.5s';
|
||||
|
||||
// Create a new XMLHttpRequest
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('PUT', endpoint);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
|
||||
xhr.onload = () => {
|
||||
// Clear animation
|
||||
this.style.animation = '';
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.status === "ok") {
|
||||
// Toggle the button state
|
||||
const newState = !isActive;
|
||||
this.setAttribute('data-active', newState.toString());
|
||||
|
||||
// Update button text and icon
|
||||
if (newState) {
|
||||
this.innerHTML = '<i class="fas fa-pause"></i> Pause Agent';
|
||||
} else {
|
||||
this.innerHTML = '<i class="fas fa-play"></i> Start Agent';
|
||||
}
|
||||
|
||||
// Show success toast
|
||||
const action = isActive ? 'pause' : 'start';
|
||||
showToast(`Agent "${agent}" ${action}ed successfully`, 'success');
|
||||
|
||||
} else if (response.error) {
|
||||
// Show error toast
|
||||
showToast(`Error: ${response.error}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle parsing error
|
||||
showToast("Invalid response format", 'error');
|
||||
console.error("Error parsing response:", e);
|
||||
}
|
||||
} else {
|
||||
// Handle HTTP error
|
||||
showToast(`Server error: ${xhr.status}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
// Clear animation
|
||||
this.style.animation = '';
|
||||
showToast("Network error occurred", 'error');
|
||||
console.error("Network error occurred");
|
||||
};
|
||||
|
||||
// Send the request
|
||||
xhr.send(JSON.stringify({}));
|
||||
});
|
||||
}
|
||||
|
||||
// Set current date for timestamp
|
||||
const now = new Date();
|
||||
document.getElementById('current-date').textContent = now.toISOString().split('T')[0];
|
||||
|
||||
// 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 = '<i class="fas fa-spinner fa-spin"></i> Updating...';
|
||||
updateButton.disabled = true;
|
||||
|
||||
// Build a structured data object
|
||||
const formData = new FormData(form);
|
||||
const jsonData = AgentFormUtils.processFormData(formData);
|
||||
|
||||
// Process special fields
|
||||
jsonData.connectors = AgentFormUtils.processConnectors(updateButton);
|
||||
if (jsonData.connectors === null) return; // Validation failed
|
||||
|
||||
jsonData.mcp_servers = AgentFormUtils.processMCPServers();
|
||||
|
||||
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);
|
||||
|
||||
// Send the structured data as JSON
|
||||
fetch(`/api/agent/${jsonData.name}/config`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(jsonData)
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error(err.error || `Server error: ${response.status}`);
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Restore button state
|
||||
updateButton.innerHTML = originalButtonText;
|
||||
updateButton.disabled = false;
|
||||
|
||||
if (data.status === "ok") {
|
||||
// Show success toast
|
||||
showToast('Agent updated successfully!', 'success');
|
||||
|
||||
// Reload agent config to get updated values
|
||||
setTimeout(() => {
|
||||
loadAgentConfig();
|
||||
}, 500);
|
||||
} else if (data.error) {
|
||||
// Show error toast
|
||||
showToast('Error: ' + data.error, 'error');
|
||||
} else {
|
||||
// Handle unexpected response format
|
||||
showToast('Unexpected response format', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Handle network or other errors
|
||||
showToast('Error: ' + error.message, 'error');
|
||||
console.error('Update error:', error);
|
||||
|
||||
// Restore button state
|
||||
updateButton.innerHTML = originalButtonText;
|
||||
updateButton.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Function to handle API responses for delete action
|
||||
function handleActionResponse(event, button) {
|
||||
const xhr = event.detail.xhr;
|
||||
const action = button.getAttribute('data-action');
|
||||
const agent = button.getAttribute('data-agent');
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.status === "ok") {
|
||||
// Action successful
|
||||
let message = "";
|
||||
|
||||
switch(action) {
|
||||
case 'delete':
|
||||
message = `Agent "${agent}" deleted successfully`;
|
||||
// Redirect to agent list page after short delay for delete
|
||||
setTimeout(() => {
|
||||
window.location.href = "/agents";
|
||||
}, 2000);
|
||||
break;
|
||||
default:
|
||||
message = "Operation completed successfully";
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showToast(message, 'success');
|
||||
|
||||
} else if (response.error) {
|
||||
// Show error message
|
||||
showToast(`Error: ${response.error}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle JSON parsing error
|
||||
showToast("Invalid response format", 'error');
|
||||
}
|
||||
} else {
|
||||
// Handle HTTP error
|
||||
showToast(`Server error: ${xhr.status}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load agent configuration from server
|
||||
function loadAgentConfig() {
|
||||
const agentName = document.getElementById('name').value;
|
||||
fetch(`/api/agent/${agentName}/config`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load agent config: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
agentConfig = data;
|
||||
populateFormWithConfig(data);
|
||||
showToast('Agent configuration loaded', 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading agent config:', error);
|
||||
showToast('Error loading agent configuration: ' + error.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Populate form with agent configuration
|
||||
function populateFormWithConfig(config) {
|
||||
// Clear existing dynamic sections
|
||||
document.getElementById('connectorsSection').innerHTML = '';
|
||||
document.getElementById('mcpSection').innerHTML = '';
|
||||
document.getElementById('action_box').innerHTML = '';
|
||||
document.getElementById('dynamic_box').innerHTML = '';
|
||||
|
||||
// Populate simple fields
|
||||
document.getElementById('hud').checked = config.hud || false;
|
||||
document.getElementById('enable_kb').checked = config.enable_kb || false;
|
||||
document.getElementById('enable_reasoning').checked = config.enable_reasoning || false;
|
||||
document.getElementById('kb_results').value = config.kb_results || '';
|
||||
document.getElementById('standalone_job').checked = config.standalone_job || false;
|
||||
document.getElementById('initiate_conversations').checked = config.initiate_conversations || false;
|
||||
document.getElementById('enable_planning').checked = config.enable_planning || false;
|
||||
document.getElementById('can_stop_itself').checked = config.can_stop_itself || false;
|
||||
document.getElementById('random_identity').checked = config.random_identity || false;
|
||||
document.getElementById('long_term_memory').checked = config.long_term_memory || false;
|
||||
document.getElementById('summary_long_term_memory').checked = config.summary_long_term_memory || false;
|
||||
document.getElementById('identity_guidance').value = config.identity_guidance || '';
|
||||
document.getElementById('description').value = config.description || '';
|
||||
document.getElementById('periodic_runs').value = config.periodic_runs || '';
|
||||
document.getElementById('model').value = config.model || '';
|
||||
document.getElementById('multimodal_model').value = config.multimodal_model || '';
|
||||
document.getElementById('api_url').value = config.api_url || '';
|
||||
document.getElementById('api_key').value = config.api_key || '';
|
||||
document.getElementById('local_rag_url').value = config.local_rag_url || '';
|
||||
document.getElementById('local_rag_api_key').value = config.local_rag_token || '';
|
||||
document.getElementById('permanent_goal').value = config.permanent_goal || '';
|
||||
document.getElementById('system_prompt').value = config.system_prompt || '';
|
||||
|
||||
// Populate connectors
|
||||
if (config.connectors && Array.isArray(config.connectors)) {
|
||||
config.connectors.forEach((connector, index) => {
|
||||
// Add connector section
|
||||
document.getElementById('addConnectorButton').click();
|
||||
|
||||
// Find the added connector elements
|
||||
const connectorType = document.getElementById(`connectorType${index}`);
|
||||
|
||||
// Set values
|
||||
if (connectorType) {
|
||||
// First set the connector type
|
||||
AgentFormUtils.setSelectValue(connectorType, connector.type);
|
||||
|
||||
// Parse the config if it's a string (from backend)
|
||||
let configObj = connector.config;
|
||||
if (typeof connector.config === 'string') {
|
||||
try {
|
||||
configObj = JSON.parse(connector.config);
|
||||
} catch (e) {
|
||||
console.error('Error parsing connector config:', e);
|
||||
configObj = {}; // Fallback to empty object if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
// Now render the appropriate form for this connector type with the config values
|
||||
AgentFormUtils.renderConnectorForm(index, connector.type, configObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Populate MCP servers
|
||||
if (config.mcp_servers && Array.isArray(config.mcp_servers)) {
|
||||
config.mcp_servers.forEach((server, index) => {
|
||||
// Add MCP server section
|
||||
document.getElementById('addMCPButton').click();
|
||||
|
||||
// Find the added MCP server elements
|
||||
const mcpURL = document.getElementById(`mcpURL${index}`);
|
||||
const mcpToken = document.getElementById(`mcpToken${index}`);
|
||||
|
||||
// Set values
|
||||
if (mcpURL) {
|
||||
// If server is a string (old format), use it as URL
|
||||
if (typeof server === 'string') {
|
||||
mcpURL.value = server;
|
||||
}
|
||||
// If server is an object (new format), extract URL
|
||||
else if (typeof server === 'object' && server !== null) {
|
||||
mcpURL.value = server.url || '';
|
||||
|
||||
if (mcpToken && server.token) {
|
||||
mcpToken.value = server.token;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Populate actions
|
||||
if (config.actions && Array.isArray(config.actions)) {
|
||||
config.actions.forEach((action, index) => {
|
||||
// Add action section
|
||||
document.getElementById('action_button').click();
|
||||
|
||||
// Find the added action elements
|
||||
const actionName = document.getElementById(`actionsName${index}`);
|
||||
const actionConfig = document.getElementById(`actionsConfig${index}`);
|
||||
|
||||
// Set values
|
||||
if (actionName) {
|
||||
AgentFormUtils.setSelectValue(actionName, action.name);
|
||||
}
|
||||
|
||||
if (actionConfig) {
|
||||
// Format the config value
|
||||
AgentFormUtils.formatConfigValue(actionConfig, action.config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Populate prompt blocks
|
||||
if (config.promptblocks && Array.isArray(config.promptblocks)) {
|
||||
config.promptblocks.forEach((block, index) => {
|
||||
// Add prompt block section
|
||||
document.getElementById('dynamic_button').click();
|
||||
|
||||
// Find the added prompt block elements
|
||||
const promptName = document.getElementById(`promptName${index}`);
|
||||
const promptConfig = document.getElementById(`promptConfig${index}`);
|
||||
|
||||
// Set values
|
||||
if (promptName) {
|
||||
AgentFormUtils.setSelectValue(promptName, block.name);
|
||||
}
|
||||
|
||||
if (promptConfig) {
|
||||
// Format the config value
|
||||
AgentFormUtils.formatConfigValue(promptConfig, block.config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,61 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Smart Agent status</title>
|
||||
{{template "views/partials/header"}}
|
||||
<style>
|
||||
body { overflow: hidden; }
|
||||
.chat-container { height: 90vh; display: flex; flex-direction: column; }
|
||||
.chat-messages { overflow-y: auto; flex-grow: 1; }
|
||||
.htmx-indicator{
|
||||
opacity:0;
|
||||
transition: opacity 10ms ease-in;
|
||||
}
|
||||
.htmx-request .htmx-indicator{
|
||||
opacity:1
|
||||
}
|
||||
/* Loader (https://cssloaders.github.io/) */
|
||||
.loader {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
margin:15px auto;
|
||||
position: relative;
|
||||
color: #FFF;
|
||||
box-sizing: border-box;
|
||||
animation: animloader 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
|
||||
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
|
||||
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 p-4 text-white font-sans" hx-ext="sse" sse-connect="/sse/{{.Name}}">
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="chat-container bg-gray-800 shadow-lg rounded-lg" >
|
||||
<!-- Chat Header -->
|
||||
<div class="border-b border-gray-700 p-4">
|
||||
<h1 class="text-lg font-semibold">{{.Name}}</h1>
|
||||
</div>
|
||||
<!-- Chat Messages -->
|
||||
<div class="chat-messages p-4">
|
||||
<div sse-swap="status" hx-swap="afterbegin" id="status"></div>
|
||||
{{ range .History }}
|
||||
<!-- Agent Status Box -->
|
||||
<div class="bg-gray-700 p-4">
|
||||
<h2 class="text-sm font-semibold">Agent:</h2>
|
||||
<div id="agentStatus" class="text-sm text-gray-300">
|
||||
Result: {{.Result}} Action: {{.Action}} Params: {{.Params}} Reasoning: {{.Reasoning}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user