refactoring
This commit is contained in:
84
webui/actions.go
Normal file
84
webui/actions.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mudler/local-agent-framework/core/action"
|
||||
"github.com/mudler/local-agent-framework/core/state"
|
||||
"github.com/mudler/local-agent-framework/pkg/xlog"
|
||||
|
||||
"github.com/mudler/local-agent-framework/core/agent"
|
||||
"github.com/mudler/local-agent-framework/services/actions"
|
||||
)
|
||||
|
||||
const (
|
||||
// Actions
|
||||
ActionSearch = "search"
|
||||
ActionCustom = "custom"
|
||||
ActionGithubIssueLabeler = "github-issue-labeler"
|
||||
ActionGithubIssueOpener = "github-issue-opener"
|
||||
ActionGithubIssueCloser = "github-issue-closer"
|
||||
ActionGithubIssueSearcher = "github-issue-searcher"
|
||||
ActionScraper = "scraper"
|
||||
ActionWikipedia = "wikipedia"
|
||||
ActionBrowse = "browse"
|
||||
ActionSendMail = "send_mail"
|
||||
)
|
||||
|
||||
var AvailableActions = []string{
|
||||
ActionSearch,
|
||||
ActionCustom,
|
||||
ActionGithubIssueLabeler,
|
||||
ActionGithubIssueOpener,
|
||||
ActionGithubIssueCloser,
|
||||
ActionGithubIssueSearcher,
|
||||
ActionScraper,
|
||||
ActionBrowse,
|
||||
ActionWikipedia,
|
||||
ActionSendMail,
|
||||
}
|
||||
|
||||
func Actions(a *state.AgentConfig) func(ctx context.Context) []agent.Action {
|
||||
return func(ctx context.Context) []agent.Action {
|
||||
allActions := []agent.Action{}
|
||||
|
||||
for _, a := range a.Actions {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(a.Config), &config); err != nil {
|
||||
xlog.Error("Error unmarshalling action config", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch a.Name {
|
||||
case ActionCustom:
|
||||
customAction, err := action.NewCustom(config, "")
|
||||
if err != nil {
|
||||
xlog.Error("Error creating custom action", "error", err)
|
||||
continue
|
||||
}
|
||||
allActions = append(allActions, customAction)
|
||||
case ActionSearch:
|
||||
allActions = append(allActions, actions.NewSearch(config))
|
||||
case ActionGithubIssueLabeler:
|
||||
allActions = append(allActions, actions.NewGithubIssueLabeler(ctx, config))
|
||||
case ActionGithubIssueOpener:
|
||||
allActions = append(allActions, actions.NewGithubIssueOpener(ctx, config))
|
||||
case ActionGithubIssueCloser:
|
||||
allActions = append(allActions, actions.NewGithubIssueCloser(ctx, config))
|
||||
case ActionGithubIssueSearcher:
|
||||
allActions = append(allActions, actions.NewGithubIssueSearch(ctx, config))
|
||||
case ActionScraper:
|
||||
allActions = append(allActions, actions.NewScraper(config))
|
||||
case ActionWikipedia:
|
||||
allActions = append(allActions, actions.NewWikipedia(config))
|
||||
case ActionBrowse:
|
||||
allActions = append(allActions, actions.NewBrowse(config))
|
||||
case ActionSendMail:
|
||||
allActions = append(allActions, actions.NewSendMail(config))
|
||||
}
|
||||
}
|
||||
|
||||
return allActions
|
||||
}
|
||||
}
|
||||
393
webui/app.go
Normal file
393
webui/app.go
Normal file
@@ -0,0 +1,393 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mudler/local-agent-framework/pkg/xlog"
|
||||
|
||||
"github.com/mudler/local-agent-framework/core/agent"
|
||||
"github.com/mudler/local-agent-framework/core/sse"
|
||||
"github.com/mudler/local-agent-framework/core/state"
|
||||
|
||||
"github.com/donseba/go-htmx"
|
||||
"github.com/dslipak/pdf"
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/template/html/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
App struct {
|
||||
htmx *htmx.HTMX
|
||||
config *Config
|
||||
*fiber.App
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
a := &App{
|
||||
htmx: htmx.New(),
|
||||
config: config,
|
||||
App: webapp,
|
||||
}
|
||||
|
||||
a.registerRoutes(config.Pool, webapp)
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *App) KnowledgeBaseReset(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
db := pool.GetAgentMemory(c.Params("name"))
|
||||
db.Reset()
|
||||
return c.Redirect("/knowledgebase/" + c.Params("name"))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) KnowledgeBaseExport(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
db := pool.GetAgentMemory(c.Params("name"))
|
||||
knowledgeBase := db.Data()
|
||||
|
||||
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.knowledgebase.json", c.Params("name")))
|
||||
return c.JSON(knowledgeBase)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) KnowledgeBaseImport(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
// Handle error
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll("./uploads", os.ModePerm)
|
||||
|
||||
destination := fmt.Sprintf("./uploads/%s", file.Filename)
|
||||
if err := c.SaveFile(file, destination); err != nil {
|
||||
// Handle error
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
knowledge := []string{}
|
||||
if err := json.Unmarshal(data, &knowledge); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(knowledge) > 0 {
|
||||
xlog.Info("Importing agent KB")
|
||||
db := pool.GetAgentMemory(c.Params("name"))
|
||||
db.Reset()
|
||||
|
||||
for _, k := range knowledge {
|
||||
db.Store(k)
|
||||
}
|
||||
|
||||
} else {
|
||||
return fmt.Errorf("Empty knowledge base")
|
||||
}
|
||||
|
||||
return c.Redirect("/agents")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) KnowledgeBaseFile(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
agent := pool.GetAgent(c.Params("name"))
|
||||
db := agent.Memory()
|
||||
|
||||
// https://golang.withcodeexample.com/blog/file-upload-handling-golang-fiber-guide/
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
// Handle error
|
||||
return err
|
||||
}
|
||||
|
||||
payload := struct {
|
||||
ChunkSize int `form:"chunk_size"`
|
||||
}{}
|
||||
|
||||
if err := c.BodyParser(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll("./uploads", os.ModePerm)
|
||||
|
||||
destination := fmt.Sprintf("./uploads/%s", file.Filename)
|
||||
if err := c.SaveFile(file, destination); err != nil {
|
||||
// Handle error
|
||||
return err
|
||||
}
|
||||
|
||||
xlog.Info("File uploaded to: " + destination)
|
||||
fmt.Printf("Payload: %+v\n", payload)
|
||||
|
||||
content, err := readPdf(destination) // Read local pdf file
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
xlog.Info("Content is", content)
|
||||
chunkSize := a.config.DefaultChunkSize
|
||||
if payload.ChunkSize > 0 {
|
||||
chunkSize = payload.ChunkSize
|
||||
}
|
||||
|
||||
go state.StringsToKB(db, chunkSize, content)
|
||||
|
||||
_, err = c.WriteString(chatDiv("File uploaded", "gray"))
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) KnowledgeBase(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
agent := pool.GetAgent(c.Params("name"))
|
||||
db := agent.Memory()
|
||||
|
||||
payload := struct {
|
||||
URL string `form:"url"`
|
||||
ChunkSize int `form:"chunk_size"`
|
||||
}{}
|
||||
|
||||
if err := c.BodyParser(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
website := payload.URL
|
||||
if website == "" {
|
||||
return fmt.Errorf("please enter a URL")
|
||||
}
|
||||
chunkSize := a.config.DefaultChunkSize
|
||||
if payload.ChunkSize > 0 {
|
||||
chunkSize = payload.ChunkSize
|
||||
}
|
||||
|
||||
go state.WebsiteToKB(website, chunkSize, db)
|
||||
|
||||
return c.Redirect("/knowledgebase/" + c.Params("name"))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Notify(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
payload := struct {
|
||||
Message string `form:"message"`
|
||||
}{}
|
||||
|
||||
if err := c.BodyParser(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := payload.Message
|
||||
if query == "" {
|
||||
_, _ = c.Write([]byte("Please enter a message."))
|
||||
return nil
|
||||
}
|
||||
|
||||
a := pool.GetAgent(c.Params("name"))
|
||||
a.Ask(
|
||||
agent.WithText(query),
|
||||
)
|
||||
_, _ = c.Write([]byte("Message sent"))
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Delete(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if err := pool.Remove(c.Params("name")); err != nil {
|
||||
xlog.Info("Error removing agent", err)
|
||||
return c.Status(http.StatusInternalServerError).SendString(err.Error())
|
||||
}
|
||||
return c.Redirect("/agents")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Pause(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
xlog.Info("Pausing agent", c.Params("name"))
|
||||
agent := pool.GetAgent(c.Params("name"))
|
||||
if agent != nil {
|
||||
agent.Pause()
|
||||
}
|
||||
return c.Redirect("/agents")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Start(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
agent := pool.GetAgent(c.Params("name"))
|
||||
if agent != nil {
|
||||
agent.Resume()
|
||||
}
|
||||
return c.Redirect("/agents")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Create(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
config := state.AgentConfig{}
|
||||
if err := c.BodyParser(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Agent configuration: %+v\n", config)
|
||||
|
||||
if config.Name == "" {
|
||||
c.Status(http.StatusBadRequest).SendString("Name is required")
|
||||
return nil
|
||||
}
|
||||
if err := pool.CreateAgent(config.Name, &config); err != nil {
|
||||
c.Status(http.StatusInternalServerError).SendString(err.Error())
|
||||
return nil
|
||||
}
|
||||
return c.Redirect("/agents")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ExportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
agent := pool.GetConfig(c.Params("name"))
|
||||
if agent == nil {
|
||||
return c.Status(http.StatusNotFound).SendString("Agent not found")
|
||||
}
|
||||
|
||||
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.json", agent.Name))
|
||||
return c.JSON(agent)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) ImportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||
return func(c *fiber.Ctx) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
// Handle error
|
||||
return err
|
||||
}
|
||||
|
||||
os.MkdirAll("./uploads", os.ModePerm)
|
||||
|
||||
destination := fmt.Sprintf("./uploads/%s", file.Filename)
|
||||
if err := c.SaveFile(file, destination); err != nil {
|
||||
// Handle error
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := state.AgentConfig{}
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xlog.Info("Importing agent", config.Name)
|
||||
|
||||
if config.Name == "" {
|
||||
c.Status(http.StatusBadRequest).SendString("Name is required")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := pool.CreateAgent(config.Name, &config); err != nil {
|
||||
c.Status(http.StatusInternalServerError).SendString(err.Error())
|
||||
return nil
|
||||
}
|
||||
return c.Redirect("/agents")
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
agent.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
|
||||
}
|
||||
}
|
||||
|
||||
func readPdf(path string) (string, error) {
|
||||
r, err := pdf.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
b, err := r.GetPlainText()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf.ReadFrom(b)
|
||||
return buf.String(), nil
|
||||
}
|
||||
58
webui/connectors.go
Normal file
58
webui/connectors.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mudler/local-agent-framework/pkg/xlog"
|
||||
"github.com/mudler/local-agent-framework/services/connectors"
|
||||
|
||||
"github.com/mudler/local-agent-framework/core/state"
|
||||
)
|
||||
|
||||
const (
|
||||
// Connectors
|
||||
ConnectorTelegram = "telegram"
|
||||
ConnectorSlack = "slack"
|
||||
ConnectorDiscord = "discord"
|
||||
ConnectorGithubIssues = "github-issues"
|
||||
ConnectorGithubPRs = "github-prs"
|
||||
)
|
||||
|
||||
var AvailableConnectors = []string{
|
||||
ConnectorTelegram,
|
||||
ConnectorSlack,
|
||||
ConnectorDiscord,
|
||||
ConnectorGithubIssues,
|
||||
ConnectorGithubPRs,
|
||||
}
|
||||
|
||||
func Connectors(a *state.AgentConfig) []state.Connector {
|
||||
conns := []state.Connector{}
|
||||
|
||||
for _, c := range a.Connector {
|
||||
var config map[string]string
|
||||
if err := json.Unmarshal([]byte(c.Config), &config); err != nil {
|
||||
xlog.Info("Error unmarshalling connector config", err)
|
||||
continue
|
||||
}
|
||||
switch c.Type {
|
||||
case ConnectorTelegram:
|
||||
cc, err := connectors.NewTelegramConnector(config)
|
||||
if err != nil {
|
||||
xlog.Info("Error creating telegram connector", err)
|
||||
continue
|
||||
}
|
||||
|
||||
conns = append(conns, cc)
|
||||
case ConnectorSlack:
|
||||
conns = append(conns, connectors.NewSlack(config))
|
||||
case ConnectorDiscord:
|
||||
conns = append(conns, connectors.NewDiscord(config))
|
||||
case ConnectorGithubIssues:
|
||||
conns = append(conns, connectors.NewGithubIssueWatcher(config))
|
||||
case ConnectorGithubPRs:
|
||||
conns = append(conns, connectors.NewGithubPRWatcher(config))
|
||||
}
|
||||
}
|
||||
return conns
|
||||
}
|
||||
39
webui/elements.go
Normal file
39
webui/elements.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
elem "github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
)
|
||||
|
||||
func chatDiv(content string, color string) string {
|
||||
div := elem.Div(attrs.Props{
|
||||
// attrs.ID: "container",
|
||||
attrs.Class: fmt.Sprintf("p-2 my-2 rounded bg-%s-600", color),
|
||||
},
|
||||
elem.Raw(htmlIfy(content)),
|
||||
)
|
||||
return div.Render()
|
||||
}
|
||||
|
||||
func loader() string {
|
||||
return elem.Div(attrs.Props{
|
||||
attrs.Class: "loader",
|
||||
}).Render()
|
||||
}
|
||||
|
||||
func disabledElement(id string, disabled bool) string {
|
||||
return elem.Script(nil,
|
||||
elem.If(disabled,
|
||||
elem.Raw(`document.getElementById('`+id+`').disabled = true`),
|
||||
elem.Raw(`document.getElementById('`+id+`').disabled = false`),
|
||||
)).Render()
|
||||
}
|
||||
|
||||
func htmlIfy(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.ReplaceAll(s, "\n", "<br>")
|
||||
return s
|
||||
}
|
||||
36
webui/options.go
Normal file
36
webui/options.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package webui
|
||||
|
||||
import "github.com/mudler/local-agent-framework/core/state"
|
||||
|
||||
type Config struct {
|
||||
DefaultChunkSize int
|
||||
Pool *state.AgentPool
|
||||
}
|
||||
|
||||
type Option func(*Config)
|
||||
|
||||
func WithDefaultChunkSize(size int) Option {
|
||||
return func(c *Config) {
|
||||
c.DefaultChunkSize = size
|
||||
}
|
||||
}
|
||||
|
||||
func WithPool(pool *state.AgentPool) Option {
|
||||
return func(c *Config) {
|
||||
c.Pool = pool
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Apply(opts ...Option) {
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfig(opts ...Option) *Config {
|
||||
c := &Config{
|
||||
DefaultChunkSize: 2048,
|
||||
}
|
||||
c.Apply(opts...)
|
||||
return c
|
||||
}
|
||||
BIN
webui/public/dash.png
Normal file
BIN
webui/public/dash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
webui/public/dash2.png
Normal file
BIN
webui/public/dash2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
BIN
webui/public/logo_1.png
Normal file
BIN
webui/public/logo_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 341 KiB |
140
webui/routes.go
Normal file
140
webui/routes.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
fiber "github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/mudler/local-agent-framework/core/agent"
|
||||
"github.com/mudler/local-agent-framework/core/sse"
|
||||
"github.com/mudler/local-agent-framework/core/state"
|
||||
)
|
||||
|
||||
//go:embed views/*
|
||||
var viewsfs embed.FS
|
||||
|
||||
//go:embed public/*
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
||||
|
||||
webapp.Use("/public", filesystem.New(filesystem.Config{
|
||||
Root: http.FS(embeddedFiles),
|
||||
PathPrefix: "public",
|
||||
Browse: true,
|
||||
}))
|
||||
|
||||
webapp.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/index", fiber.Map{
|
||||
"Agents": pool.List(),
|
||||
})
|
||||
})
|
||||
|
||||
webapp.Get("/agents", func(c *fiber.Ctx) error {
|
||||
statuses := map[string]bool{}
|
||||
for _, a := range pool.List() {
|
||||
statuses[a] = !pool.GetAgent(a).Paused()
|
||||
}
|
||||
return c.Render("views/agents", fiber.Map{
|
||||
"Agents": pool.List(),
|
||||
"Status": statuses,
|
||||
})
|
||||
})
|
||||
|
||||
webapp.Get("/create", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/create", fiber.Map{
|
||||
"Actions": AvailableActions,
|
||||
"Connectors": AvailableConnectors,
|
||||
})
|
||||
})
|
||||
|
||||
webapp.Get("/knowledgebase/:name", func(c *fiber.Ctx) error {
|
||||
db := pool.GetAgentMemory(c.Params("name"))
|
||||
return c.Render(
|
||||
"views/knowledgebase",
|
||||
fiber.Map{
|
||||
"KnowledgebaseItemsCount": db.Count(),
|
||||
"Name": c.Params("name"),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// 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 {
|
||||
return c.SendStatus(404)
|
||||
}
|
||||
|
||||
m.Handle(c, sse.NewClient(randStringRunes(10)))
|
||||
return nil
|
||||
})
|
||||
|
||||
webapp.Get("/status/:name", func(c *fiber.Ctx) error {
|
||||
history := pool.GetStatusHistory(c.Params("name"))
|
||||
if history == nil {
|
||||
history = &state.Status{ActionResults: []agent.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("/create", app.Create(pool))
|
||||
webapp.Get("/delete/:name", app.Delete(pool))
|
||||
webapp.Put("/pause/:name", app.Pause(pool))
|
||||
webapp.Put("/start/:name", app.Start(pool))
|
||||
|
||||
webapp.Post("/knowledgebase/:name", app.KnowledgeBase(pool))
|
||||
webapp.Post("/knowledgebase/:name/upload", app.KnowledgeBaseFile(pool))
|
||||
webapp.Delete("/knowledgebase/:name/reset", app.KnowledgeBaseReset(pool))
|
||||
webapp.Post("/knowledgebase/:name/import", app.KnowledgeBaseImport(pool))
|
||||
webapp.Get("/knowledgebase/:name/export", app.KnowledgeBaseExport(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 {
|
||||
return c.Render("views/settings", fiber.Map{
|
||||
// "Character": agent.Character,
|
||||
"Name": c.Params("name"),
|
||||
})
|
||||
})
|
||||
webapp.Post("/settings/import", app.ImportAgent(pool))
|
||||
webapp.Get("/settings/export/:name", app.ExportAgent(pool))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randStringRunes(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Reverse[T any](original []T) (reversed []T) {
|
||||
reversed = make([]T, len(original))
|
||||
copy(reversed, original)
|
||||
|
||||
for i := len(reversed)/2 - 1; i >= 0; i-- {
|
||||
tmp := len(reversed) - 1 - i
|
||||
reversed[i], reversed[tmp] = reversed[tmp], reversed[i]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
111
webui/views/agents.html
Normal file
111
webui/views/agents.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<!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>
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px; /* Adjusts the spacing to your liking */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 p-4 text-white font-sans">
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-3xl md:text-5xl font-bold">Smart Agent List</h1>
|
||||
</header>
|
||||
<div class="button-container">
|
||||
<a href="/create" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition duration-300 ease-in-out">
|
||||
Add New Agent
|
||||
</a>
|
||||
</div>
|
||||
<section class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="py-2 align-middle inline-block min-w-full">
|
||||
<div class="shadow overflow-hidden border-b border-gray-700 rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center">
|
||||
<span class="sr-only">Talk</span>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center">
|
||||
<span class="sr-only">Start</span>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center">
|
||||
<span class="sr-only">Stop</span>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center">
|
||||
<span class="sr-only">Settings</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-gray-800 divide-y divide-gray-700">
|
||||
<!-- Dynamic agent rows go here -->
|
||||
{{ $status := .Status }}
|
||||
{{ range .Agents }}
|
||||
<tr hx-ext="sse">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-300">{{.}}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
|
||||
Online: {{ index $status . }}
|
||||
<a href="/status/{{.}}" class="text-indigo-500 hover:text-indigo-400">
|
||||
<i class="fas fa-info"></i> Status
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<a href="/talk/{{.}}" class="text-indigo-500 hover:text-indigo-400">
|
||||
<i class="fas fa-comments"></i> Talk
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<button hx-put="/start/{{.}}">
|
||||
Start
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<button hx-put="/pause/{{.}}">
|
||||
Pause
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<a href="/settings/{{.}}" class="text-indigo-500 hover:text-indigo-400">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<!-- Repeat for each agent -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="mt-8 text-center">
|
||||
|
||||
|
||||
<!-- File Upload Form -->
|
||||
<div class="section-box">
|
||||
<form id='form' hx-encoding='multipart/form-data' hx-post='/settings/import'>
|
||||
<h2>Import</h2>
|
||||
<label for="file">File:</label>
|
||||
<input type='file' name='file' id='file'>
|
||||
<button type="submit">Upload File</button>
|
||||
<progress id='progress' value='0' max='100'></progress>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
91
webui/views/chat.html
Normal file
91
webui/views/chat.html
Normal file
@@ -0,0 +1,91 @@
|
||||
<!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>
|
||||
138
webui/views/create.html
Normal file
138
webui/views/create.html
Normal file
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Create New Agent</title>
|
||||
{{template "views/partials/header"}}
|
||||
<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>
|
||||
</head>
|
||||
<body class="bg-gray-900 p-4 text-white font-sans">
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="max-w-2xl mx-auto my-12 bg-gray-800 p-8 rounded-lg shadow-lg">
|
||||
<h1 class="text-3xl font-bold text-center mb-10 text-blue-400">Create New Agent</h1>
|
||||
|
||||
<form action="/create" method="POST" class="space-y-6">
|
||||
|
||||
<div class="mb-6">
|
||||
<label for="name" class="block text-lg font-medium text-gray-400">Name</label>
|
||||
<input type="text" name="name" id="name" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white" placeholder="Name">
|
||||
</div>
|
||||
|
||||
<div id="connectorsSection">
|
||||
</div>
|
||||
<button type="button" id="addConnectorButton" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Add Connector
|
||||
</button>
|
||||
|
||||
<script>
|
||||
const actions = `{{ range .Actions }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
const connectors = `{{ range .Connectors }}<option value="{{.}}">{{.}}</option>{{ end }}`;
|
||||
|
||||
document.getElementById('addConnectorButton').addEventListener('click', function() {
|
||||
const connectorsSection = document.getElementById('connectorsSection');
|
||||
const newConnectorIndex = connectorsSection.getElementsByClassName('connector').length;
|
||||
const newConnectorHTML = `
|
||||
<div class="connector mb-4">
|
||||
<div class="mb-4">
|
||||
<label for="connectorType${newConnectorIndex}" class="block text-lg font-medium text-gray-400">Connector Type</label>
|
||||
<select name="connectors[${newConnectorIndex}].type" id="connectorType${newConnectorIndex}" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
`+connectors+` </select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="connectorConfig${newConnectorIndex}" class="block text-lg font-medium text-gray-400">Connector Config (JSON)</label>
|
||||
<textarea id="connectorConfig${newConnectorIndex}" name="connectors[${newConnectorIndex}].config" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white" placeholder='{"token":"sk-mg3.."}'>{}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
connectorsSection.insertAdjacentHTML('beforeend', newConnectorHTML);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4" id="action_box">
|
||||
</div>
|
||||
<button id="action_button" type="button" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Add action</button>
|
||||
|
||||
<script>
|
||||
|
||||
document.getElementById('action_button').addEventListener('click', function() {
|
||||
const actionsSection = document.getElementById('action_box');
|
||||
const ii = actionsSection.getElementsByClassName('action').length;
|
||||
|
||||
const newActionHTML = `
|
||||
<div class="action mb-4">
|
||||
<label for="actionsName${ii}" class="block text-lg font-medium text-gray-400">Action</label>
|
||||
<select name="actions[${ii}].name" id="actionsName${ii}" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
`+actions+`</select>
|
||||
<div class="mb-4">
|
||||
<label for="actionsConfig${ii}" class="block text-lg font-medium text-gray-400">Action Config (JSON)</label>
|
||||
<textarea id="actionsConfig${ii}" name="actions[${ii}].config" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white" placeholder='{"results":"5"}'>{}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
actionsSection.insertAdjacentHTML('beforeend', newActionHTML);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="hud" class="block text-lg font-medium text-gray-400">HUD</label>
|
||||
<input type="checkbox" name="hud" id="hud" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="enable_kb" class="block text-lg font-medium text-gray-400">Enable Knowledge Base</label>
|
||||
<input type="checkbox" name="enable_kb" id="enable_kb" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
<label for="enable_reasoning" class="block text-lg font-medium text-gray-400">Enable Reasoning</label>
|
||||
<input type="checkbox" name="enable_reasoning" id="enable_reasoning" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
<div class="mb-6">
|
||||
<label for="kb_results" class="block text-lg font-medium text-gray-400">Knowledge base results</label>
|
||||
<input type="text" name="kb_results" id="kb_results" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white" placeholder="3">
|
||||
</div>
|
||||
|
||||
<label for="standalone_job" class="block text-lg font-medium text-gray-400">Standalone Job</label>
|
||||
<input type="checkbox" name="standalone_job" id="standalone_job" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
|
||||
<label for="initiate_conversations" class="block text-lg font-medium text-gray-400">Initiate conversations</label>
|
||||
<input type="checkbox" name="random_identity" id="initiate_conversations" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
|
||||
<label for="can_stop_itself" class="block text-lg font-medium text-gray-400">Can stop itself</label>
|
||||
<input type="checkbox" name="can_stop_itself" id="can_stop_itself" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
|
||||
<label for="random_identity" class="block text-lg font-medium text-gray-400">Random Identity</label>
|
||||
<input type="checkbox" name="random_identity" id="random_identity" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
|
||||
<label for="long_term_memory" class="block text-lg font-medium text-gray-400">Long term memory</label>
|
||||
<input type="checkbox" name="long_term_memory" id="long_term_memory" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
<label for="summary_long_term_memory" class="block text-lg font-medium text-gray-400">Long term memory (summarize!)</label>
|
||||
<input type="checkbox" name="summary_long_term_memory" id="summary_long_term_memory" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="identity_guidance" class="block text-lg font-medium text-gray-400">Identity Guidance</label>
|
||||
<textarea name="identity_guidance" id="identity_guidance" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-lg border-gray-300 rounded-md bg-gray-700 text-white" placeholder="Identity Guidance"></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="periodic_runs" class="block text-lg font-medium text-gray-400">Periodic Runs</label>
|
||||
<input type="text" name="periodic_runs" id="periodic_runs" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white" placeholder="Periodic Runs">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="permanent_goal" class="block text-lg font-medium text-gray-400">Permanent goal</label>
|
||||
<textarea name="permanent_goal" id="permanent_goal" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white" placeholder="Permanent goal"></textarea>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="system_prompt" class="block text-lg font-medium text-gray-400">System prompt</label>
|
||||
<textarea name="system_prompt" id="system_prompt" class="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md bg-gray-700 text-white" placeholder="System prompt"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Create Agent
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
31
webui/views/index.html
Normal file
31
webui/views/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Smart Assistant Dashboard</title>
|
||||
{{template "views/partials/header"}}
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 p-4 text-white font-sans">
|
||||
{{template "views/partials/menu"}}
|
||||
<div class="container">
|
||||
<div class="image-container">
|
||||
<img src="/public/logo_1.png" width="250" alt="Company Logo">
|
||||
</div>
|
||||
<!-- Card for Agent List Page -->
|
||||
<a href="/agents" class="card-link">
|
||||
<div class="card">
|
||||
<h2>Agent List</h2>
|
||||
<p>View and manage your list of agents, including detailed profiles and statistics.</p>
|
||||
</div>
|
||||
</a>
|
||||
<a href="/create" class="card-link">
|
||||
<div class="card">
|
||||
<h2>Create</h2>
|
||||
<p>Create a new agent.</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
64
webui/views/knowledgebase.html
Normal file
64
webui/views/knowledgebase.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Knowledgebase for {{.Name}}</title>
|
||||
{{template "views/partials/header"}}
|
||||
</head>
|
||||
<body class="bg-gray-900 p-4 text-white font-sans">
|
||||
{{template "views/partials/menu"}}
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-3xl md:text-5xl font-bold">Knowledgebase (items: {{.KnowledgebaseItemsCount}})</h1>
|
||||
</header>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Add Site Form -->
|
||||
<div class="section-box">
|
||||
<form action="/knowledgebase/{{.Name}}" method="POST">
|
||||
<h2>Add sites to KB</h2>
|
||||
<label for="url">URL:</label>
|
||||
<input type="text" name="url" id="url" placeholder="Enter URL here">
|
||||
<label for="chunk_size">Chunk size:</label>
|
||||
<input type="text" name="chunk_size" id="chunk_size" placeholder="Enter chunk size here">
|
||||
<button type="submit">Add Site</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Form -->
|
||||
<div class="section-box">
|
||||
<form id='form' hx-encoding='multipart/form-data' hx-post='/knowledgebase/{{.Name}}/upload'>
|
||||
<h2>Upload File</h2>
|
||||
<label for="file">File:</label>
|
||||
<input type='file' name='file' id='file'>
|
||||
<label for="chunk_size">Chunk size:</label>
|
||||
<input type="text" name="chunk_size" id="chunk_size" placeholder="Enter chunk size here">
|
||||
<button type="submit">Upload File</button>
|
||||
<progress id='progress' value='0' max='100'></progress>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Reset Knowledge Base -->
|
||||
<div class="section-box">
|
||||
<button hx-swap="none" hx-trigger="click" hx-delete="/knowledgebase/{{.Name}}/reset">Reset Knowledge Base</button>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Export</h2>
|
||||
<a href="/knowledgebase/{{.Name}}/export" >Export</a>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<form id='form' hx-encoding='multipart/form-data' hx-post='/knowledgebase/{{.Name}}/import'>
|
||||
<h2>Import</h2>
|
||||
<label for="file">File:</label>
|
||||
<input type='file' name='file' id='file'>
|
||||
<button type="submit">Upload File</button>
|
||||
<progress id='progress' value='0' max='100'></progress>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
htmx.on('#form', 'htmx:xhr:progress', function(evt) {
|
||||
htmx.find('#progress').setAttribute('value', evt.detail.loaded / evt.detail.total * 100);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
223
webui/views/partials/header.html
Normal file
223
webui/views/partials/header.html
Normal file
@@ -0,0 +1,223 @@
|
||||
<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 href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<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>
|
||||
|
||||
<script defer src="https://unpkg.com/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://unpkg.com/alpinejs" defer></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.section-box {
|
||||
background-color: #2a2a2a; /* Darker background for the form */
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1a1a1a; /* Lighter overall background */
|
||||
color: #ffffff;
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
input, button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="file"] {
|
||||
background-color: #333333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4a76a8; /* Blue color for buttons */
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #5a86b8;
|
||||
}
|
||||
textarea {
|
||||
height: 200px; /* Increased height for better JSON visibility */
|
||||
}
|
||||
/* Enhancing select box styles */
|
||||
select {
|
||||
appearance: none; /* Remove default system appearance */
|
||||
background-color: #333; /* Darker background for the dark theme */
|
||||
border: 2px solid #555; /* Slightly lighter border for contrast */
|
||||
color: white; /* Text color */
|
||||
padding: 10px; /* Padding inside the select box */
|
||||
border-radius: 5px; /* Rounded corners */
|
||||
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>'); /* Custom dropdown arrow using SVG */
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center; /* Positioning the arrow nicely */
|
||||
background-size: 12px; /* Size of the arrow */
|
||||
cursor: pointer; /* Cursor indicates it's clickable */
|
||||
}
|
||||
|
||||
select:hover {
|
||||
border-color: #777; /* Lighter border on hover for visibility */
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none; /* Remove default focus outline */
|
||||
border-color: #1e90ff; /* Focus color similar to the hover background of buttons */
|
||||
box-shadow: 0 0 3px #1e90ff; /* Adding a slight glow effect */
|
||||
}
|
||||
|
||||
select {
|
||||
/* Previous styles */
|
||||
overflow-y: auto; /* Ensures that a scrollbar is available when needed */
|
||||
}
|
||||
|
||||
option {
|
||||
background-color: #333; /* Dark background for each option */
|
||||
color: white; /* Light color for the text */
|
||||
padding: 8px 10px; /* Padding for each option */
|
||||
}
|
||||
|
||||
/* Adding a hover effect for options is not consistently supported across all browsers */
|
||||
select:hover option {
|
||||
background-color: #444; /* Slightly lighter background on hover */
|
||||
}
|
||||
|
||||
/* Custom Scrollbars for the dropdown */
|
||||
select {
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: #666 #333; /* Firefox: thumb and track color */
|
||||
}
|
||||
|
||||
select::-webkit-scrollbar {
|
||||
width: 8px; /* For WebKit browsers */
|
||||
}
|
||||
|
||||
select::-webkit-scrollbar-track {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
select::-webkit-scrollbar-thumb {
|
||||
background-color: #666;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
/* Basic setup for the checkbox */
|
||||
.checkbox-custom {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px; /* Width of the checkbox */
|
||||
height: 20px; /* Height of the checkbox */
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Hide the default checkbox input */
|
||||
.checkbox-custom input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Create a custom box */
|
||||
.checkbox-custom .checkmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: #444; /* Dark grey box */
|
||||
border-radius: 4px; /* Rounded corners for the box */
|
||||
border: 1px solid #777; /* Slightly lighter border */
|
||||
}
|
||||
|
||||
/* On mouse-over, add a different border color */
|
||||
.checkbox-custom:hover .checkmark {
|
||||
border-color: #aaa;
|
||||
}
|
||||
|
||||
/* When the checkbox is checked, change the background and insert a checkmark */
|
||||
.checkbox-custom input:checked ~ .checkmark {
|
||||
background-color: #1e90ff; /* Blue background for checked state */
|
||||
border-color: #1e90ff;
|
||||
}
|
||||
|
||||
/* Create the checkmark using a pseudo element */
|
||||
.checkbox-custom .checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the checkmark when checked */
|
||||
.checkbox-custom input:checked ~ .checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Style the checkmark */
|
||||
.checkbox-custom .checkmark:after {
|
||||
left: 7px;
|
||||
top: 3px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 3px 3px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.card-link {
|
||||
text-decoration: none; /* Removes underline from links */
|
||||
}
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px auto;
|
||||
text-align: left;
|
||||
width: 90%;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for hover effects */
|
||||
display: block; /* Ensures the link fills the card */
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px); /* Slight lift effect */
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); /* Shadow for depth */
|
||||
}
|
||||
.card h2 {
|
||||
font-size: 1.5em; /* Larger and more prominent */
|
||||
font-weight: bold; /* Ensures boldness */
|
||||
color: white; /* Ensures visibility against the card's background */
|
||||
margin-bottom: 0.5em; /* Space below the heading */
|
||||
}
|
||||
.card a,
|
||||
.card p {
|
||||
color: white; /* Ensures text color is consistent */
|
||||
}
|
||||
.card p {
|
||||
font-size: 1em;
|
||||
}
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
22
webui/views/partials/menu.html
Normal file
22
webui/views/partials/menu.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<nav class="bg-gray-800 w-full">
|
||||
<div class="px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo container -->
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<!-- Replace 'logo.png' with the actual path to your logo image -->
|
||||
<a href="/" >
|
||||
<img src="/public/logo_1.png" alt="Logo" class="h-8 w-auto mr-3">
|
||||
</a>
|
||||
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-white bg-gray-900 focus:outline-none focus:text-white focus:bg-gray-700">
|
||||
<i class="fas fa-home"></i> LocalAgent
|
||||
</a>
|
||||
<a href="/agents" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:text-white focus:bg-gray-700">
|
||||
<i class="fas fa-users"></i> Agent list
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<br>
|
||||
86
webui/views/settings.html
Normal file
86
webui/views/settings.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Agent settings {{.Name}}</title>
|
||||
{{template "views/partials/header"}}
|
||||
<style>
|
||||
.section-box {
|
||||
background-color: #2a2a2a; /* Darker background for the form */
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #1a1a1a; /* Lighter overall background */
|
||||
color: #ffffff;
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
input, button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="file"] {
|
||||
background-color: #333333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4a76a8; /* Blue color for buttons */
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #5a86b8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{template "views/partials/menu"}}
|
||||
<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">
|
||||
<a href="/knowledgebase/{{.Name}}" class="card-link">
|
||||
<div class="card">
|
||||
<h2>Manage Knowledgebase</h2>
|
||||
<p>Access and update your knowledgebase to improve agent responses and efficiency.</p>
|
||||
</div>
|
||||
</a>
|
||||
<div class="section-box">
|
||||
<button hx-put="/start/{{.Name}}" class="text-indigo-500 hover:text-indigo-400">
|
||||
Start
|
||||
</button>
|
||||
<button hx-put="/pause/{{.Name}}" class="text-indigo-500 hover:text-indigo-400">
|
||||
Pause
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Export</h2>
|
||||
<a href="/settings/export/{{.Name}}" >Export</a>
|
||||
</div>
|
||||
|
||||
<div class="section-box">
|
||||
<h2>Danger section</h2>
|
||||
<a href="/delete/{{.Name}}" class="text-red-500 hover:text-red-400">
|
||||
<i class="fas fa-trash-alt"></i> Delete
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
htmx.on('#form', 'htmx:xhr:progress', function(evt) {
|
||||
htmx.find('#progress').setAttribute('value', evt.detail.loaded / evt.detail.total * 100);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
61
webui/views/status.html
Normal file
61
webui/views/status.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!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