General Settings
+ +Default: ~/.picoclaw/workspace
+Model List
+ +Channels
+ +Telegram
+ +Discord
+ +More channels available in the raw config editor.
+From 0b9051853ecab7f651bc6666a63103160487a82b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:09:10 +0000 Subject: [PATCH] feat: add web-based configuration dashboard Implemented a new `dashboard` command that starts a web server for easy, user-friendly configuration of the AI agent. - Backend: Go HTTP server with API endpoints for reading/writing config. - Frontend: Modern, responsive dashboard using Tailwind CSS and Alpine.js. - CLI: Integrated `picoclaw dashboard` command with host/port options. - UI: Forms for general settings, model management, channels, tools, and heartbeat. - Advanced: Raw JSON editor for full control. Addresses the request for a seamless web-based configuration experience. Co-authored-by: Xeven777 <115650165+Xeven777@users.noreply.github.com> --- cmd/picoclaw/internal/dashboard/command.go | 31 ++ cmd/picoclaw/internal/dashboard/helpers.go | 110 ++++++ .../internal/dashboard/web/index.html | 343 ++++++++++++++++++ cmd/picoclaw/main.go | 2 + cmd/picoclaw/main_test.go | 1 + 5 files changed, 487 insertions(+) create mode 100644 cmd/picoclaw/internal/dashboard/command.go create mode 100644 cmd/picoclaw/internal/dashboard/helpers.go create mode 100644 cmd/picoclaw/internal/dashboard/web/index.html diff --git a/cmd/picoclaw/internal/dashboard/command.go b/cmd/picoclaw/internal/dashboard/command.go new file mode 100644 index 000000000..880ba8dd0 --- /dev/null +++ b/cmd/picoclaw/internal/dashboard/command.go @@ -0,0 +1,31 @@ +package dashboard + +import ( + "embed" + + "github.com/spf13/cobra" +) + +//go:embed web/index.html +var embeddedFiles embed.FS + +func NewDashboardCommand() *cobra.Command { + var host string + var port int + var noBrowser bool + + cmd := &cobra.Command{ + Use: "dashboard", + Aliases: []string{"d", "ui"}, + Short: "Start the web-based configuration dashboard", + RunE: func(cmd *cobra.Command, args []string) error { + return runDashboard(host, port, !noBrowser) + }, + } + + cmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host to bind to") + cmd.Flags().IntVarP(&port, "port", "p", 18795, "Port to listen on") + cmd.Flags().BoolVar(&noBrowser, "no-browser", false, "Do not open the browser automatically") + + return cmd +} diff --git a/cmd/picoclaw/internal/dashboard/helpers.go b/cmd/picoclaw/internal/dashboard/helpers.go new file mode 100644 index 000000000..6ac1d4b50 --- /dev/null +++ b/cmd/picoclaw/internal/dashboard/helpers.go @@ -0,0 +1,110 @@ +package dashboard + +import ( + "encoding/json" + "fmt" + "net/http" + "os/exec" + "runtime" + "time" + + "github.com/sipeed/picoclaw/cmd/picoclaw/internal" + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/logger" +) + +func runDashboard(host string, port int, openBrowser bool) error { + addr := fmt.Sprintf("%s:%d", host, port) + url := fmt.Sprintf("http://%s", addr) + if host == "0.0.0.0" { + url = fmt.Sprintf("http://localhost:%d", port) + } + + mux := http.NewServeMux() + + // API Handlers + mux.HandleFunc("/api/config", configHandler) + + // Static Assets + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + data, err := embeddedFiles.ReadFile("web/index.html") + if err != nil { + http.Error(w, "Failed to read index.html", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html") + w.Write(data) + }) + + server := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + } + + fmt.Printf("🚀 PicoClaw Dashboard starting on %s\n", url) + + if openBrowser { + go func() { + time.Sleep(500 * time.Millisecond) + openInBrowser(url) + }() + } + + return server.ListenAndServe() +} + +func configHandler(w http.ResponseWriter, r *http.Request) { + configPath := internal.GetConfigPath() + + switch r.Method { + case http.MethodGet: + cfg, err := internal.LoadConfig() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(cfg) + + case http.MethodPost: + var cfg config.Config + if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + if err := config.SaveConfig(configPath, &cfg); err != nil { + http.Error(w, "Failed to save config: "+err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "OK") + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func openInBrowser(url string) { + var err error + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + if err != nil { + logger.WarnCF("dashboard", "Failed to open browser", map[string]any{"error": err.Error(), "url": url}) + } +} diff --git a/cmd/picoclaw/internal/dashboard/web/index.html b/cmd/picoclaw/internal/dashboard/web/index.html new file mode 100644 index 000000000..e1d013845 --- /dev/null +++ b/cmd/picoclaw/internal/dashboard/web/index.html @@ -0,0 +1,343 @@ + + +
+ + +Default: ~/.picoclaw/workspace
+More channels available in the raw config editor.
+