Skip to content

Commit 3ebcc14

Browse files
feat: add HuggingChat support (#477)
* add chat ui to dashboard and docker compose & refactor dashboard/backend/ Signed-off-by: JaredforReal <[email protected]> * try fix network error Signed-off-by: JaredforReal <[email protected]> * more --------- Signed-off-by: JaredforReal <[email protected]> Co-authored-by: bitliu <[email protected]>
1 parent 792a1ef commit 3ebcc14

File tree

16 files changed

+1061
-475
lines changed

16 files changed

+1061
-475
lines changed

dashboard/backend/config/config.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package config
2+
3+
import (
4+
"flag"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// Config holds all application configuration
10+
type Config struct {
11+
Port string
12+
StaticDir string
13+
ConfigFile string
14+
AbsConfigPath string
15+
ConfigDir string
16+
17+
// Upstream targets
18+
GrafanaURL string
19+
PrometheusURL string
20+
RouterAPIURL string
21+
RouterMetrics string
22+
OpenWebUIURL string
23+
ChatUIURL string
24+
JaegerURL string
25+
}
26+
27+
// env returns the env var or default
28+
func env(key, def string) string {
29+
if v := os.Getenv(key); v != "" {
30+
return v
31+
}
32+
return def
33+
}
34+
35+
// LoadConfig loads configuration from flags and environment variables
36+
func LoadConfig() (*Config, error) {
37+
cfg := &Config{}
38+
39+
// Flags/env for configuration
40+
port := flag.String("port", env("DASHBOARD_PORT", "8700"), "dashboard port")
41+
staticDir := flag.String("static", env("DASHBOARD_STATIC_DIR", "../frontend"), "static assets directory")
42+
configFile := flag.String("config", env("ROUTER_CONFIG_PATH", "../../config/config.yaml"), "path to config.yaml")
43+
44+
// Upstream targets
45+
grafanaURL := flag.String("grafana", env("TARGET_GRAFANA_URL", ""), "Grafana base URL")
46+
promURL := flag.String("prometheus", env("TARGET_PROMETHEUS_URL", ""), "Prometheus base URL")
47+
routerAPI := flag.String("router_api", env("TARGET_ROUTER_API_URL", "http://localhost:8080"), "Router API base URL")
48+
routerMetrics := flag.String("router_metrics", env("TARGET_ROUTER_METRICS_URL", "http://localhost:9190/metrics"), "Router metrics URL")
49+
openwebuiURL := flag.String("openwebui", env("TARGET_OPENWEBUI_URL", ""), "Open WebUI base URL")
50+
chatuiURL := flag.String("chatui", env("TARGET_CHATUI_URL", ""), "Hugging Face Chat UI base URL")
51+
jaegerURL := flag.String("jaeger", env("TARGET_JAEGER_URL", ""), "Jaeger base URL")
52+
53+
flag.Parse()
54+
55+
cfg.Port = *port
56+
cfg.StaticDir = *staticDir
57+
cfg.ConfigFile = *configFile
58+
cfg.GrafanaURL = *grafanaURL
59+
cfg.PrometheusURL = *promURL
60+
cfg.RouterAPIURL = *routerAPI
61+
cfg.RouterMetrics = *routerMetrics
62+
cfg.OpenWebUIURL = *openwebuiURL
63+
cfg.ChatUIURL = *chatuiURL
64+
cfg.JaegerURL = *jaegerURL
65+
66+
// Resolve config file path to absolute path
67+
absConfigPath, err := filepath.Abs(cfg.ConfigFile)
68+
if err != nil {
69+
return nil, err
70+
}
71+
cfg.AbsConfigPath = absConfigPath
72+
cfg.ConfigDir = filepath.Dir(absConfigPath)
73+
74+
return cfg, nil
75+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
10+
yaml "gopkg.in/yaml.v3"
11+
)
12+
13+
// ConfigHandler reads and serves the config.yaml file as JSON
14+
func ConfigHandler(configPath string) http.HandlerFunc {
15+
return func(w http.ResponseWriter, r *http.Request) {
16+
// Only allow GET requests
17+
if r.Method != http.MethodGet {
18+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
19+
return
20+
}
21+
22+
// Read the config file
23+
data, err := os.ReadFile(configPath)
24+
if err != nil {
25+
log.Printf("Error reading config file: %v", err)
26+
http.Error(w, fmt.Sprintf("Failed to read config file: %v", err), http.StatusInternalServerError)
27+
return
28+
}
29+
30+
// Parse YAML
31+
var config interface{}
32+
if err := yaml.Unmarshal(data, &config); err != nil {
33+
log.Printf("Error parsing config YAML: %v", err)
34+
http.Error(w, fmt.Sprintf("Failed to parse config: %v", err), http.StatusInternalServerError)
35+
return
36+
}
37+
38+
// Convert to JSON and send response
39+
w.Header().Set("Content-Type", "application/json")
40+
if err := json.NewEncoder(w).Encode(config); err != nil {
41+
log.Printf("Error encoding config to JSON: %v", err)
42+
http.Error(w, fmt.Sprintf("Failed to encode config: %v", err), http.StatusInternalServerError)
43+
return
44+
}
45+
}
46+
}
47+
48+
// UpdateConfigHandler updates the config.yaml file
49+
func UpdateConfigHandler(configPath string) http.HandlerFunc {
50+
return func(w http.ResponseWriter, r *http.Request) {
51+
// Only allow POST/PUT requests
52+
if r.Method != http.MethodPost && r.Method != http.MethodPut {
53+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
54+
return
55+
}
56+
57+
// Read the request body
58+
var configData map[string]interface{}
59+
if err := json.NewDecoder(r.Body).Decode(&configData); err != nil {
60+
log.Printf("Error decoding request body: %v", err)
61+
http.Error(w, fmt.Sprintf("Invalid request body: %v", err), http.StatusBadRequest)
62+
return
63+
}
64+
65+
// Convert to YAML
66+
yamlData, err := yaml.Marshal(configData)
67+
if err != nil {
68+
log.Printf("Error marshaling config to YAML: %v", err)
69+
http.Error(w, fmt.Sprintf("Failed to convert config to YAML: %v", err), http.StatusInternalServerError)
70+
return
71+
}
72+
73+
// Write to file
74+
if err := os.WriteFile(configPath, yamlData, 0644); err != nil {
75+
log.Printf("Error writing config file: %v", err)
76+
http.Error(w, fmt.Sprintf("Failed to write config file: %v", err), http.StatusInternalServerError)
77+
return
78+
}
79+
80+
log.Printf("Configuration updated successfully")
81+
w.Header().Set("Content-Type", "application/json")
82+
w.WriteHeader(http.StatusOK)
83+
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "Configuration updated successfully"})
84+
}
85+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package handlers
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// HealthCheck handles health check endpoint
8+
func HealthCheck(w http.ResponseWriter, r *http.Request) {
9+
w.Header().Set("Content-Type", "application/json")
10+
w.WriteHeader(http.StatusOK)
11+
w.Write([]byte(`{"status":"healthy","service":"semantic-router-dashboard"}`))
12+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package handlers
2+
3+
import (
4+
"net/http"
5+
"os"
6+
"path"
7+
"strings"
8+
)
9+
10+
// StaticFileServer serves static files and handles SPA routing
11+
func StaticFileServer(staticDir string) http.Handler {
12+
fs := http.FileServer(http.Dir(staticDir))
13+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
// Never serve index.html for API or embedded proxy routes
15+
// These should be handled by their respective handlers
16+
p := r.URL.Path
17+
// Never serve static files for proxy routes or ChatUI API endpoints
18+
if strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/embedded/") ||
19+
strings.HasPrefix(p, "/metrics/") || strings.HasPrefix(p, "/public/") ||
20+
strings.HasPrefix(p, "/avatar/") || strings.HasPrefix(p, "/_app/") ||
21+
strings.HasPrefix(p, "/_next/") || strings.HasPrefix(p, "/chatui/") ||
22+
p == "/conversation" || strings.HasPrefix(p, "/conversations") ||
23+
strings.HasPrefix(p, "/settings") || p == "/login" || p == "/logout" ||
24+
strings.HasPrefix(p, "/r/") {
25+
// These paths should have been handled by other handlers
26+
// If we reach here, it means the proxy failed or route not found
27+
w.Header().Set("Content-Type", "application/json")
28+
http.Error(w, `{"error":"Route not found","message":"This path should have been handled by a proxy"}`, http.StatusBadGateway)
29+
return
30+
}
31+
32+
full := path.Join(staticDir, path.Clean(p))
33+
34+
// Check if file exists
35+
info, err := os.Stat(full)
36+
if err == nil {
37+
// File exists
38+
if !info.IsDir() {
39+
// It's a file, serve it
40+
fs.ServeHTTP(w, r)
41+
return
42+
}
43+
// It's a directory, try index.html
44+
indexPath := path.Join(full, "index.html")
45+
if _, err := os.Stat(indexPath); err == nil {
46+
http.ServeFile(w, r, indexPath)
47+
return
48+
}
49+
}
50+
51+
// File doesn't exist or is directory without index.html
52+
// For SPA routing: serve index.html for routes without file extension
53+
if !strings.Contains(path.Base(p), ".") {
54+
http.ServeFile(w, r, path.Join(staticDir, "index.html"))
55+
return
56+
}
57+
58+
// Otherwise let the file server handle it (will return 404)
59+
fs.ServeHTTP(w, r)
60+
})
61+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
)
11+
12+
// ToolsDBHandler reads and serves the tools_db.json file
13+
func ToolsDBHandler(configDir string) http.HandlerFunc {
14+
return func(w http.ResponseWriter, r *http.Request) {
15+
// Only allow GET requests
16+
if r.Method != http.MethodGet {
17+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
18+
return
19+
}
20+
21+
// Construct the tools_db.json path
22+
toolsDBPath := filepath.Join(configDir, "tools_db.json")
23+
24+
// Read the tools database file
25+
data, err := os.ReadFile(toolsDBPath)
26+
if err != nil {
27+
log.Printf("Error reading tools_db.json: %v", err)
28+
http.Error(w, fmt.Sprintf("Failed to read tools database: %v", err), http.StatusInternalServerError)
29+
return
30+
}
31+
32+
// Parse JSON to validate it
33+
var tools interface{}
34+
if err := json.Unmarshal(data, &tools); err != nil {
35+
log.Printf("Error parsing tools_db.json: %v", err)
36+
http.Error(w, fmt.Sprintf("Failed to parse tools database: %v", err), http.StatusInternalServerError)
37+
return
38+
}
39+
40+
// Send response
41+
w.Header().Set("Content-Type", "application/json")
42+
if err := json.NewEncoder(w).Encode(tools); err != nil {
43+
log.Printf("Error encoding tools to JSON: %v", err)
44+
http.Error(w, fmt.Sprintf("Failed to encode tools: %v", err), http.StatusInternalServerError)
45+
return
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)