diff --git a/cmd/ui-server/server.go b/cmd/ui-server/server.go index 776fcd6..c663e72 100644 --- a/cmd/ui-server/server.go +++ b/cmd/ui-server/server.go @@ -19,6 +19,7 @@ import ( "github.com/go-go-golems/clay/pkg/watcher" "github.com/go-go-golems/go-go-mcp/pkg/events" "github.com/go-go-golems/go-go-mcp/pkg/server/sse" + "github.com/go-go-golems/go-go-mcp/pkg/server/ui" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" "gopkg.in/yaml.v3" @@ -26,7 +27,7 @@ import ( type Server struct { dir string - pages map[string]UIDefinition + pages map[string]ui.UIDefinition routes map[string]http.HandlerFunc watcher *watcher.Watcher mux *http.ServeMux @@ -35,10 +36,7 @@ type Server struct { subscriber message.Subscriber events events.EventManager sseHandler *sse.SSEHandler -} - -type UIDefinition struct { - Components []map[string]interface{} `yaml:"components"` + uiHandler *ui.UIHandler } func NewServer(dir string) (*Server, error) { @@ -57,15 +55,19 @@ func NewServer(dir string) (*Server, error) { // Create SSE handler sseHandler := sse.NewSSEHandler(eventManager, &log.Logger) + // Create UI handler + uiHandler := ui.NewUIHandler(eventManager, sseHandler, &log.Logger) + s := &Server{ dir: dir, - pages: make(map[string]UIDefinition), + pages: make(map[string]ui.UIDefinition), routes: make(map[string]http.HandlerFunc), mux: http.NewServeMux(), publisher: publisher, subscriber: publisher, events: eventManager, sseHandler: sseHandler, + uiHandler: uiHandler, } // Register component renderers @@ -88,14 +90,8 @@ func NewServer(dir string) (*Server, error) { // Set up static file handler s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - // Set up UI update endpoint - s.mux.Handle("/api/ui-update", s.handleUIUpdate()) - - // Set up UI action endpoint - s.mux.Handle("/api/ui-action", s.handleUIAction()) - - // Set up UI update page - s.mux.HandleFunc("/ui", s.handleUIUpdatePage()) + // Register UI handlers + uiHandler.RegisterHandlers(s.mux) // Set up dynamic page handler - must come before index handler s.mux.Handle("/pages/", s.handleAllPages()) @@ -178,7 +174,7 @@ func (s *Server) loadPages() error { // First, clear existing pages and routes s.mu.Lock() - s.pages = make(map[string]UIDefinition) + s.pages = make(map[string]ui.UIDefinition) s.routes = make(map[string]http.HandlerFunc) s.mu.Unlock() @@ -212,7 +208,7 @@ func (s *Server) loadPage(path string) error { return fmt.Errorf("failed to read file %s: %w", path, err) } - var def UIDefinition + var def ui.UIDefinition if err := yaml.Unmarshal(data, &def); err != nil { log.Error().Err(err).Str("path", path).Msg("Failed to parse YAML") return fmt.Errorf("failed to parse YAML in %s: %w", path, err) @@ -337,7 +333,7 @@ func (s *Server) registerComponentRenderers() { log.Debug().Str("pageID", pageID).Msg("Rendering page content template") // Get the page definition - var def UIDefinition + var def ui.UIDefinition // Try to extract definition from event data if compData, ok := data.(map[string]interface{}); ok { @@ -371,284 +367,3 @@ func (s *Server) registerComponentRenderers() { return buf.String(), nil }) } - -// handleUIUpdate handles POST requests to /api/ui-update -func (s *Server) handleUIUpdate() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Only accept POST requests - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Parse JSON body - var jsonData map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "json_parse_error", - "message": "Invalid JSON: " + err.Error(), - }, - }) - return - } - - // Convert to YAML for storage - yamlData, err := yaml.Marshal(jsonData) - if err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "yaml_conversion_error", - "message": "Failed to convert to YAML: " + err.Error(), - }, - }) - return - } - - // Parse into UIDefinition - var def UIDefinition - if err := yaml.Unmarshal(yamlData, &def); err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "ui_definition_error", - "message": "Invalid UI definition: " + err.Error(), - "yaml": string(yamlData), - }, - }) - return - } - - // Validate the UI definition - validationErrors := s.validateUIDefinition(def) - if len(validationErrors) > 0 { - // Return detailed JSON error response with validation errors - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "ui_validation_error", - "message": "UI definition validation failed", - "details": validationErrors, - }, - }) - return - } - - // Create and publish refresh-ui event - event := events.UIEvent{ - Type: "refresh-ui", - PageID: "ui-update", - Component: def, - } - - if err := s.events.Publish("ui-update", event); err != nil { - // Return detailed JSON error response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "type": "event_publish_error", - "message": "Failed to publish event: " + err.Error(), - }, - }) - return - } - - // Return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - if err != nil { - http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) - } - } -} - -// validateUIDefinition checks a UI definition for common errors -func (s *Server) validateUIDefinition(def UIDefinition) []map[string]interface{} { - var errors []map[string]interface{} - - // Check if components exist - if len(def.Components) == 0 { - errors = append(errors, map[string]interface{}{ - "path": "components", - "message": "No components defined", - }) - return errors - } - - // Validate each component - for i, comp := range def.Components { - for typ, props := range comp { - propsMap, ok := props.(map[string]interface{}) - if !ok { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s", i, typ), - "message": "Component properties must be a map", - }) - continue - } - - // Check for required ID - if _, hasID := propsMap["id"]; !hasID && requiresID(typ) { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s", i, typ), - "message": "Component is missing required 'id' property", - }) - } - - // Validate nested components in forms - if typ == "form" { - if formComps, hasComps := propsMap["components"]; hasComps { - if formCompsList, ok := formComps.([]interface{}); ok { - for j, formComp := range formCompsList { - if formCompMap, ok := formComp.(map[string]interface{}); ok { - for formCompType, formCompProps := range formCompMap { - if _, ok := formCompProps.(map[string]interface{}); !ok { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.components[%d].%s", i, typ, j, formCompType), - "message": "Form component properties must be a map", - }) - } - } - } else { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.components[%d]", i, typ, j), - "message": "Form component must be a map", - }) - } - } - } else { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.components", i, typ), - "message": "Form components must be an array", - }) - } - } - } - - // Validate list items - if typ == "list" { - if items, hasItems := propsMap["items"]; hasItems { - if itemsList, ok := items.([]interface{}); ok { - for j, item := range itemsList { - if _, ok := item.(map[string]interface{}); !ok { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.items[%d]", i, typ, j), - "message": "List item must be a map", - }) - } - } - } else { - errors = append(errors, map[string]interface{}{ - "path": fmt.Sprintf("components[%d].%s.items", i, typ), - "message": "List items must be an array", - }) - } - } - } - } - } - - return errors -} - -// requiresID returns true if the component type requires an ID -func requiresID(componentType string) bool { - switch componentType { - case "text", "title": - // These can optionally have IDs - return false - default: - // All other components require IDs - return true - } -} - -// handleUIAction handles POST requests to /api/ui-action -func (s *Server) handleUIAction() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Only accept POST requests - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Parse JSON body - var action struct { - ComponentID string `json:"componentId"` - Action string `json:"action"` - Data map[string]interface{} `json:"data"` - } - - if err := json.NewDecoder(r.Body).Decode(&action); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - return - } - - // Determine if this is an important event to log at INFO level - isImportantEvent := false - switch action.Action { - case "clicked", "changed", "submitted": - isImportantEvent = true - } - - // Log the action at appropriate level - logger := log.Debug() - if isImportantEvent { - logger = log.Info() - } - - // Create log entry with component and action info - logger = logger. - Str("componentId", action.ComponentID). - Str("action", action.Action) - - // Add data to log if it exists and is relevant - if len(action.Data) > 0 { - // For form submissions, log the form data in detail - if action.Action == "submitted" && action.Data["formData"] != nil { - logger = logger.Interface("formData", action.Data["formData"]) - } else if action.Action == "changed" { - // For changed events, log the new value - logger = logger.Interface("data", action.Data) - } else { - // For other events, just log that data exists - logger = logger.Bool("hasData", true) - } - } - - // Output the log message - logger.Msg("UI action received") - - // Return success response - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - if err != nil { - http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) - } - } -} - -// handleUIUpdatePage renders the UI update page -func (s *Server) handleUIUpdatePage() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - component := uiUpdateTemplate() - _ = component.Render(r.Context(), w) - } -} diff --git a/cmd/ui-server/templates.templ b/cmd/ui-server/templates.templ index 6ada651..21586fd 100644 --- a/cmd/ui-server/templates.templ +++ b/cmd/ui-server/templates.templ @@ -5,6 +5,7 @@ import ( "gopkg.in/yaml.v3" "strings" "github.com/rs/zerolog/log" + "github.com/go-go-golems/go-go-mcp/pkg/server/ui" ) templ base(title string) { @@ -48,7 +49,10 @@ templ base(title string) { } function sendUIAction(componentId, action, data = {}) { - logToConsole(`Component ${componentId} ${action}`); + // Get the request ID from the page if it exists + const requestId = document.querySelector('[data-request-id]')?.getAttribute('data-request-id') || ''; + + logToConsole(`Component ${componentId} ${action}${requestId ? ' (request: ' + requestId + ')' : ''}`); // If this is a form submission, collect all form data if (action === 'submitted' && document.getElementById(componentId)) { @@ -94,7 +98,8 @@ templ base(title string) { body: JSON.stringify({ componentId: componentId, action: action, - data: data + data: data, + requestId: requestId // Include the request ID in the action data }) }) .then(response => response.json()) @@ -187,7 +192,7 @@ templ base(title string) { } -templ indexTemplate(pages map[string]UIDefinition) { +templ indexTemplate(pages map[string]ui.UIDefinition) { @base("UI Server - Index") {
@@ -204,7 +209,7 @@ templ indexTemplate(pages map[string]UIDefinition) { } } -templ pageTemplate(name string, def UIDefinition) { +templ pageTemplate(name string, def ui.UIDefinition) { @base("UI Server - " + name) {
Rendered UI
-
+
for _, component := range def.Components { for typ, props := range component { @renderComponent(typ, props.(map[string]interface{})) @@ -453,7 +458,7 @@ templ renderComponent(typ string, props map[string]interface{}) { } } -func yamlString(def UIDefinition) string { +func yamlString(def ui.UIDefinition) string { yamlBytes, err := yaml.Marshal(def) if err != nil { errMsg := fmt.Sprintf("Error marshaling YAML: %v", err) @@ -476,6 +481,7 @@ templ uiUpdateTemplate() { hx-ext="sse" sse-connect="/sse?page=ui-update" sse-swap="ui-update" + data-request-id="" >
Waiting for UI updates... @@ -493,6 +499,14 @@ templ uiUpdateTemplate() { // Re-highlight the code hljs.highlightElement(el); }); + + // Extract request ID from the updated content if available + const newRequestId = event.detail.target.querySelector('[data-request-id]')?.getAttribute('data-request-id'); + if (newRequestId) { + // Update the parent container with the new request ID + event.detail.target.closest('[data-request-id]')?.setAttribute('data-request-id', newRequestId); + console.log('Updated request ID to:', newRequestId); + } }); } diff --git a/cmd/ui-server/templates_templ.go b/cmd/ui-server/templates_templ.go index 2c7057f..4d7f8b4 100644 --- a/cmd/ui-server/templates_templ.go +++ b/cmd/ui-server/templates_templ.go @@ -10,6 +10,7 @@ import templruntime "github.com/a-h/templ/runtime" import ( "fmt" + "github.com/go-go-golems/go-go-mcp/pkg/server/ui" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" "strings" @@ -43,7 +44,7 @@ func base(title string) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 16, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 17, Col: 17} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -65,7 +66,7 @@ func base(title string) templ.Component { }) } -func indexTemplate(pages map[string]UIDefinition) templ.Component { +func indexTemplate(pages map[string]ui.UIDefinition) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -119,7 +120,7 @@ func indexTemplate(pages map[string]UIDefinition) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 198, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 203, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -144,7 +145,7 @@ func indexTemplate(pages map[string]UIDefinition) templ.Component { }) } -func pageTemplate(name string, def UIDefinition) templ.Component { +func pageTemplate(name string, def ui.UIDefinition) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -184,7 +185,7 @@ func pageTemplate(name string, def UIDefinition) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("/sse?page=" + name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 211, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 216, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -212,7 +213,7 @@ func pageTemplate(name string, def UIDefinition) templ.Component { }) } -func pageContentTemplate(name string, def UIDefinition) templ.Component { +func pageContentTemplate(name string, def ui.UIDefinition) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -237,6 +238,19 @@ func pageContentTemplate(name string, def UIDefinition) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(def.RequestID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 231, Col: 58} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 13) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } for _, component := range def.Components { for typ, props := range component { templ_7745c5c3_Err = renderComponent(typ, props.(map[string]interface{})).Render(ctx, templ_7745c5c3_Buffer) @@ -245,20 +259,20 @@ func pageContentTemplate(name string, def UIDefinition) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 13) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 14) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(yamlString(def)) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(yamlString(def)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 241, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 246, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 14) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 15) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -282,256 +296,243 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var12 := templ.GetChildren(ctx) - if templ_7745c5c3_Var12 == nil { - templ_7745c5c3_Var12 = templ.NopComponent + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent } ctx = templ.ClearChildren(ctx) switch typ { case "button": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 15) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 16) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 263, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 268, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 16) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 17) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 = []any{ + var templ_7745c5c3_Var15 = []any{ "btn", templ.KV("btn-primary", props["type"] == "primary"), templ.KV("btn-secondary", props["type"] == "secondary"), templ.KV("btn-danger", props["type"] == "danger"), templ.KV("btn-success", props["type"] == "success"), } - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 17) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 18) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 265, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 270, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 18) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 19) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 266, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 271, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 19) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 20) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if disabled, ok := props["disabled"].(bool); ok && disabled { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 20) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 21) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 21) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 22) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String()) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String()) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 22) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 23) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if text, ok := props["text"].(string); ok { - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(text) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(text) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 279, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 284, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 23) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 24) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "title": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 24) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 25) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 286, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 291, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 25) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 26) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 288, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 293, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 26) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 27) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 289, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 294, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 27) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 28) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if content, ok := props["content"].(string); ok { - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(content) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 292, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 297, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 28) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 29) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "text": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 29) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 30) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 299, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 304, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 30) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 31) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 301, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 306, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 31) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 32) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var25 string - templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'clicked')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 302, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 307, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 32) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 33) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if content, ok := props["content"].(string); ok { - var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(content) + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 305, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 310, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 33) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 34) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "input": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 34) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 312, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 35) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var28 string - templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(id) + templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 314, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 317, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -542,9 +543,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) + templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 315, Col: 95} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 319, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -555,9 +556,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 316, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 320, Col: 95} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { @@ -568,9 +569,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 317, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 321, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { @@ -580,116 +581,116 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 322, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 40) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } if typ, ok := props["type"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 40) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 41) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(typ) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(typ) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 319, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 324, Col: 17} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 41) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 42) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if placeholder, ok := props["placeholder"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 42) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 43) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 322, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 327, Col: 32} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 43) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 44) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if value, ok := props["value"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 44) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 45) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(value) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 325, Col: 20} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 330, Col: 20} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 45) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 46) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if required, ok := props["required"].(bool); ok && required { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 46) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 47) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if name, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 47) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 48) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(name) + var templ_7745c5c3_Var36 string + templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 331, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 336, Col: 18} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 48) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 49) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 49) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 50) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "textarea": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 50) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 339, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 51) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var37 string - templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(id) + templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 341, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 344, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) if templ_7745c5c3_Err != nil { @@ -700,9 +701,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) + templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 342, Col: 95} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 346, Col: 13} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) if templ_7745c5c3_Err != nil { @@ -713,9 +714,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {value: this.value})", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 343, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 347, Col: 95} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) if templ_7745c5c3_Err != nil { @@ -726,9 +727,9 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var40 string - templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'focused')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 344, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 348, Col: 73} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) if templ_7745c5c3_Err != nil { @@ -738,196 +739,209 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'blurred')", id)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 349, Col: 72} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 56) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } if rows, ok := props["rows"].(int); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 56) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 57) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(rows)) + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(rows)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 346, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 351, Col: 30} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 57) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 58) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if cols, ok := props["cols"].(int); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 58) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 59) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cols)) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(cols)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 349, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 354, Col: 30} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 59) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 60) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if placeholder, ok := props["placeholder"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 60) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 61) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(placeholder) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 352, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 357, Col: 32} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 61) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 62) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 62) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 63) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if value, ok := props["value"].(string); ok { - var templ_7745c5c3_Var44 string - templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(value) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(value) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 357, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 362, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 63) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 64) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "checkbox": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 64) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 65) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var45 string - templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var46 string + templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 364, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 369, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 65) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 66) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var47 string + templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 368, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 373, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 66) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 67) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var47 string - templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {checked: this.checked})", id)) + var templ_7745c5c3_Var48 string + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("sendUIAction('%s', 'changed', {checked: this.checked})", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 369, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 374, Col: 100} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 67) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 68) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if name, ok := props["name"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 68) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 69) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(name) + var templ_7745c5c3_Var49 string + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 371, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 376, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 69) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 70) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if checked, ok := props["checked"].(bool); ok && checked { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 70) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 71) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if required, ok := props["required"].(bool); ok && required { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 71) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 72) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 72) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 73) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if label, ok := props["label"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 73) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 74) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var49 string - templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var50 string + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 382, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 387, Col: 47} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 74) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 75) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var50 string - templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(label) + var templ_7745c5c3_Var51 string + templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 382, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 387, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 75) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 76) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 76) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 77) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -935,61 +949,61 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { case "list": if typ, ok := props["type"].(string); ok { if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 77) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 78) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var51 string - templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var52 string + templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 390, Col: 46} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 395, Col: 46} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 78) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 79) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if title, ok := props["title"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 79) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 80) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var52 string - templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(title) + var templ_7745c5c3_Var53 string + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 392, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 397, Col: 31} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 80) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 81) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if typ == "ul" { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 81) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 82) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if items, ok := props["items"].([]interface{}); ok { for _, item := range items { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 82) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 83) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch i := item.(type) { case string: - var templ_7745c5c3_Var53 string - templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(i) + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(i) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 401, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 406, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1001,35 +1015,35 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 83) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 84) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 84) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 85) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if typ == "ol" { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 85) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 86) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if items, ok := props["items"].([]interface{}); ok { for _, item := range items { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 86) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 87) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch i := item.(type) { case string: - var templ_7745c5c3_Var54 string - templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(i) + var templ_7745c5c3_Var55 string + templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(i) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 418, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 423, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1041,18 +1055,18 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 87) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 88) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 88) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 89) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 89) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 90) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1060,46 +1074,46 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } case "form": if id, ok := props["id"].(string); ok { - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 90) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 91) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var55 string - templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) + var templ_7745c5c3_Var56 string + templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("component-%s", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 434, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 439, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 91) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 92) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(id) + var templ_7745c5c3_Var57 string + templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 436, Col: 13} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 441, Col: 13} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 92) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 93) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var57 string - templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("event.preventDefault(); sendUIAction('%s', 'submitted')", id)) + var templ_7745c5c3_Var58 string + templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("event.preventDefault(); sendUIAction('%s', 'submitted')", id)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 437, Col: 100} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 442, Col: 100} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 93) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 94) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1115,7 +1129,7 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 94) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 95) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1125,7 +1139,7 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { }) } -func yamlString(def UIDefinition) string { +func yamlString(def ui.UIDefinition) string { yamlBytes, err := yaml.Marshal(def) if err != nil { errMsg := fmt.Sprintf("Error marshaling YAML: %v", err) @@ -1152,12 +1166,12 @@ func uiUpdateTemplate() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var58 := templ.GetChildren(ctx) - if templ_7745c5c3_Var58 == nil { - templ_7745c5c3_Var58 = templ.NopComponent + templ_7745c5c3_Var59 := templ.GetChildren(ctx) + if templ_7745c5c3_Var59 == nil { + templ_7745c5c3_Var59 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var59 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var60 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -1169,13 +1183,13 @@ func uiUpdateTemplate() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 95) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 96) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return templ_7745c5c3_Err }) - templ_7745c5c3_Err = base("Dynamic UI").Render(templ.WithChildren(ctx, templ_7745c5c3_Var59), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = base("Dynamic UI").Render(templ.WithChildren(ctx, templ_7745c5c3_Var60), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/cmd/ui-server/templates_templ.txt b/cmd/ui-server/templates_templ.txt index 946bacf..c7673ac 100644 --- a/cmd/ui-server/templates_templ.txt +++ b/cmd/ui-server/templates_templ.txt @@ -1,5 +1,5 @@ - +

Available Pages

-
Rendered UI
+
Rendered UI
YAML Source

 
-
Dynamic UI
Waiting for UI updates...
+
Dynamic UI
Waiting for UI updates...
diff --git a/go.mod b/go.mod index e2c31ff..26158da 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sebdah/goldie/v2 v2.5.5 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect diff --git a/go.sum b/go.sum index 651adb3..ddff081 100644 --- a/go.sum +++ b/go.sum @@ -212,11 +212,11 @@ github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgY github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= -github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= diff --git a/pkg/events/types.go b/pkg/events/types.go index 22dc79d..2467399 100644 --- a/pkg/events/types.go +++ b/pkg/events/types.go @@ -8,9 +8,10 @@ import ( // UIEvent represents an event in the UI system that can trigger updates type UIEvent struct { - Type string `json:"type"` // e.g. "component-update", "page-reload" - PageID string `json:"pageId"` // Which page is being updated - Component interface{} `json:"component"` // The updated component data + Type string `json:"type"` // e.g. "component-update", "page-reload" + PageID string `json:"pageId"` // Which page is being updated + Component interface{} `json:"component"` // The updated component data + RequestID string `json:"requestId,omitempty"` // Optional request ID for tracking user actions } // EventManager defines the interface for managing UI events diff --git a/pkg/server/ui/handler.go b/pkg/server/ui/handler.go new file mode 100644 index 0000000..8f5a578 --- /dev/null +++ b/pkg/server/ui/handler.go @@ -0,0 +1,401 @@ +package ui + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/go-go-golems/go-go-mcp/pkg/events" + "github.com/go-go-golems/go-go-mcp/pkg/server/sse" + "github.com/google/uuid" + "github.com/rs/zerolog" + "gopkg.in/yaml.v3" +) + +// UIDefinition represents a UI page definition +type UIDefinition struct { + Components []map[string]interface{} `yaml:"components"` + RequestID string `yaml:"requestID,omitempty"` +} + +// UIHandler manages all UI-related functionality +type UIHandler struct { + waitRegistry *WaitRegistry + events events.EventManager + sseHandler *sse.SSEHandler + logger *zerolog.Logger + + // Configuration + timeout time.Duration +} + +// NewUIHandler creates a new UI handler with the given dependencies +func NewUIHandler(events events.EventManager, sseHandler *sse.SSEHandler, logger *zerolog.Logger) *UIHandler { + h := &UIHandler{ + waitRegistry: NewWaitRegistry(30 * time.Second), // 30 second default timeout + events: events, + sseHandler: sseHandler, + logger: logger, + timeout: 30 * time.Second, + } + + // Start background cleanup for orphaned requests + go h.cleanupOrphanedRequests(context.Background()) + + return h +} + +// RegisterHandlers registers all UI-related HTTP handlers with the given mux +func (h *UIHandler) RegisterHandlers(mux *http.ServeMux) { + mux.Handle("/api/ui-update", h.handleUIUpdate()) + mux.Handle("/api/ui-action", h.handleUIAction()) +} + +// cleanupOrphanedRequests periodically cleans up stale requests +func (h *UIHandler) cleanupOrphanedRequests(ctx context.Context) { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Perform cleanup + count := h.waitRegistry.CleanupStale() + if count > 0 { + h.logger.Debug().Int("count", count).Msg("Cleaned up orphaned requests") + } + + case <-ctx.Done(): + return + } + } +} + +// Helper method for sending error responses +func (h *UIHandler) sendErrorResponse(w http.ResponseWriter, status int, errorType, message string, details map[string]interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + errorResponse := map[string]interface{}{ + "status": "error", + "error": map[string]interface{}{ + "type": errorType, + "message": message, + }, + } + + // Add any additional details if provided + for k, v := range details { + errorResponse["error"].(map[string]interface{})[k] = v + } + + _ = json.NewEncoder(w).Encode(errorResponse) +} + +// Placeholder methods that will be implemented later +func (h *UIHandler) handleUIUpdate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var jsonData map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil { + // Return detailed JSON error response + h.sendErrorResponse(w, http.StatusBadRequest, "json_parse_error", "Invalid JSON: "+err.Error(), nil) + return + } + + // Generate a unique request ID + requestID := uuid.New().String() + + // Add the request ID to the UI definition + // This will be used by the frontend to associate actions with this request + jsonData["requestID"] = requestID + + // Convert to YAML for storage + yamlData, err := yaml.Marshal(jsonData) + if err != nil { + h.sendErrorResponse(w, http.StatusInternalServerError, "yaml_conversion_error", "Failed to convert to YAML: "+err.Error(), nil) + return + } + + // Parse into UIDefinition + var def UIDefinition + if err := yaml.Unmarshal(yamlData, &def); err != nil { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_definition_error", "Invalid UI definition: "+err.Error(), map[string]interface{}{ + "yaml": string(yamlData), + }) + return + } + + // Validate the UI definition + validationErrors := h.validateUIDefinition(def) + if len(validationErrors) > 0 { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_validation_error", "UI definition validation failed", map[string]interface{}{ + "details": validationErrors, + }) + return + } + + // Register this request in the wait registry + responseChan := h.waitRegistry.Register(requestID) + + // Create and publish refresh-ui event with the request ID + event := events.UIEvent{ + Type: "refresh-ui", + PageID: "ui-update", + Component: def, + } + + // Add the request ID to the event data if possible + if compData, ok := event.Component.(UIDefinition); ok { + compData.RequestID = requestID + event.Component = compData + } + + if err := h.events.Publish("ui-update", event); err != nil { + // Clean up the registry entry + h.waitRegistry.Cleanup(requestID) + h.sendErrorResponse(w, http.StatusInternalServerError, "event_publish_error", "Failed to publish event: "+err.Error(), nil) + return + } + + // Log that we're waiting for a response + h.logger.Debug(). + Str("requestId", requestID). + Msg("Waiting for UI action response") + + // Wait for the response or timeout + select { + case response := <-responseChan: + // Request was resolved with a user action + if response.Error != nil { + // There was an error processing the action + h.sendErrorResponse(w, http.StatusInternalServerError, "action_processing_error", response.Error.Error(), nil) + return + } + + // Log the successful response + h.logger.Info(). + Str("requestId", requestID). + Str("action", response.Action). + Str("componentId", response.ComponentID). + Msg("UI action received, completing request") + + // Return success with the action data + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "action": response.Action, + "componentId": response.ComponentID, + "data": response.Data, + }) + + case <-time.After(h.timeout): + // Request timed out + h.waitRegistry.Cleanup(requestID) + + h.logger.Warn(). + Str("requestId", requestID). + Dur("timeout", h.timeout). + Msg("Request timed out waiting for UI action") + + h.sendErrorResponse(w, http.StatusRequestTimeout, "timeout", "No user action received within the timeout period", nil) + } + } +} + +func (h *UIHandler) handleUIAction() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var action struct { + ComponentID string `json:"componentId"` + Action string `json:"action"` + Data map[string]interface{} `json:"data"` + RequestID string `json:"requestId"` // Added field for request ID + } + + if err := json.NewDecoder(r.Body).Decode(&action); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + // Determine if this is an important event to log at INFO level + isImportantEvent := false + switch action.Action { + case "clicked", "changed", "submitted": + isImportantEvent = true + } + + // Log the action at appropriate level + logger := h.logger.Debug() + if isImportantEvent { + logger = h.logger.Info() + } + + // Create log entry with component and action info + logger = logger. + Str("componentId", action.ComponentID). + Str("action", action.Action) + + // Add request ID to log if present + if action.RequestID != "" { + logger = logger.Str("requestId", action.RequestID) + } + + // Add data to log if it exists and is relevant + if len(action.Data) > 0 { + // For form submissions, log the form data in detail + if action.Action == "submitted" && action.Data["formData"] != nil { + logger = logger.Interface("formData", action.Data["formData"]) + } else if action.Action == "changed" { + // For changed events, log the new value + logger = logger.Interface("data", action.Data) + } else { + // For other events, just log that data exists + logger = logger.Bool("hasData", true) + } + } + + // Output the log message + logger.Msg("UI action received") + + // Check if this action is associated with a waiting request + if action.RequestID != "" && (action.Action == "clicked" || action.Action == "submitted") { + // Try to resolve the waiting request + resolved := h.waitRegistry.Resolve(action.RequestID, UIActionResponse{ + Action: action.Action, + ComponentID: action.ComponentID, + Data: action.Data, + Error: nil, + Timestamp: time.Now(), + }) + + if resolved { + logger.Bool("waitingRequestResolved", true).Msg("Resolved waiting request") + } + } + + // Return success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + if err != nil { + http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } + } +} + +// validateUIDefinition checks a UI definition for common errors +func (h *UIHandler) validateUIDefinition(def UIDefinition) []map[string]interface{} { + var errors []map[string]interface{} + + // Check if components exist + if len(def.Components) == 0 { + errors = append(errors, map[string]interface{}{ + "path": "components", + "message": "No components defined", + }) + return errors + } + + // Validate each component + for i, comp := range def.Components { + for typ, props := range comp { + propsMap, ok := props.(map[string]interface{}) + if !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component properties must be a map", + }) + continue + } + + // Check for required ID + if _, hasID := propsMap["id"]; !hasID && h.requiresID(typ) { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component is missing required 'id' property", + }) + } + + // Validate nested components in forms + if typ == "form" { + if formComps, hasComps := propsMap["components"]; hasComps { + if formCompsList, ok := formComps.([]interface{}); ok { + for j, formComp := range formCompsList { + if formCompMap, ok := formComp.(map[string]interface{}); ok { + for formCompType, formCompProps := range formCompMap { + if _, ok := formCompProps.(map[string]interface{}); !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.components[%d].%s", i, typ, j, formCompType), + "message": "Form component properties must be a map", + }) + } + } + } else { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.components[%d]", i, typ, j), + "message": "Form component must be a map", + }) + } + } + } else { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.components", i, typ), + "message": "Form components must be an array", + }) + } + } + } + + // Validate list items + if typ == "list" { + if items, hasItems := propsMap["items"]; hasItems { + if itemsList, ok := items.([]interface{}); ok { + for j, item := range itemsList { + if _, ok := item.(map[string]interface{}); !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.items[%d]", i, typ, j), + "message": "List item must be a map", + }) + } + } + } else { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s.items", i, typ), + "message": "List items must be an array", + }) + } + } + } + } + } + + return errors +} + +// requiresID returns true if the component type requires an ID +func (h *UIHandler) requiresID(componentType string) bool { + switch componentType { + case "text", "title": + // These can optionally have IDs + return false + default: + // All other components require IDs + return true + } +} diff --git a/pkg/server/ui/registry.go b/pkg/server/ui/registry.go new file mode 100644 index 0000000..2257c6d --- /dev/null +++ b/pkg/server/ui/registry.go @@ -0,0 +1,73 @@ +package ui + +import ( + "sync" + "time" +) + +// UIActionResponse represents the response from a UI action +type UIActionResponse struct { + Action string + ComponentID string + Data map[string]interface{} + Error error + Timestamp time.Time +} + +// WaitRegistry tracks pending UI update requests waiting for user actions +type WaitRegistry struct { + pending map[string]chan UIActionResponse + mu sync.RWMutex + timeout time.Duration +} + +// NewWaitRegistry creates a new registry with the specified timeout +func NewWaitRegistry(timeout time.Duration) *WaitRegistry { + return &WaitRegistry{ + pending: make(map[string]chan UIActionResponse), + timeout: timeout, + } +} + +// Register adds a new request to the registry and returns a channel for the response +func (wr *WaitRegistry) Register(requestID string) chan UIActionResponse { + wr.mu.Lock() + defer wr.mu.Unlock() + + responseChan := make(chan UIActionResponse, 1) + wr.pending[requestID] = responseChan + + return responseChan +} + +// Resolve completes a pending request with the given action response +func (wr *WaitRegistry) Resolve(requestID string, response UIActionResponse) bool { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + ch <- response + delete(wr.pending, requestID) + return true + } + return false +} + +// Cleanup removes a request from the registry +func (wr *WaitRegistry) Cleanup(requestID string) { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + close(ch) + delete(wr.pending, requestID) + } +} + +// CleanupStale removes requests that have been in the registry longer than the timeout +// This is a placeholder implementation that would need to be enhanced with timestamp tracking +func (wr *WaitRegistry) CleanupStale() int { + // This would require tracking timestamps for each request + // For now, we'll just return 0 as we're not implementing the full functionality yet + return 0 +} diff --git a/ttmp/2025-02-21/changelog.md b/ttmp/2025-02-21/changelog.md index 3e24597..cf23c95 100644 --- a/ttmp/2025-02-21/changelog.md +++ b/ttmp/2025-02-21/changelog.md @@ -8,6 +8,16 @@ Enhanced the UI action handling documentation with structured metadata: - Added maintenance triggers to identify when document updates are needed - Structured questions the document answers for better discoverability +UI Action Request ID Tracking + +Implemented request ID tracking in UI actions to enable synchronous wait-for-response behavior: +- Updated sendUIAction JavaScript function to include request ID in action data +- Added request ID extraction from UI definition in page templates +- Enhanced UI update template to maintain request ID during SSE updates +- Improved logging to show request ID in console messages +- Added data attributes to store request ID in the DOM +- Ensured proper propagation of request ID between server and client + UI DSL Documentation Added comprehensive documentation for the UI DSL system in pkg/doc/topics/05-ui-dsl.md. The documentation includes: diff --git a/ttmp/2025-02-22/05-ui-action-handler.md b/ttmp/2025-02-22/05-ui-action-handler.md new file mode 100644 index 0000000..63b3706 --- /dev/null +++ b/ttmp/2025-02-22/05-ui-action-handler.md @@ -0,0 +1,686 @@ +# Technical Plan: Implementing Wait-for-Response in UI Update Endpoint + +## Overview + +The goal is to enhance the `/api/ui-update` endpoint to wait for a corresponding user action (submit or click) before completing the request. This creates a synchronous flow between UI updates and user interactions. Additionally, we'll refactor the UI handlers into their own struct to improve code organization. + +## Key Components + +1. A wait registry to track pending requests +2. UUID generation for request tracking +3. Timeout handling for requests without responses +4. A dedicated UI handler struct for better code organization +5. Frontend modifications to pass request IDs with actions + +## Detailed Implementation Plan + +### 1. Create a UIHandler Struct + +- [x] Define a new `UIHandler` struct to encapsulate all UI-related functionality +- [x] Move UI-specific methods from the Server struct to the UIHandler +- [x] Create a clean interface between Server and UIHandler + +```go +// UIHandler manages all UI-related functionality +type UIHandler struct { + waitRegistry *WaitRegistry + events events.EventManager + sseHandler *sse.SSEHandler + logger *zerolog.Logger + + // Configuration + timeout time.Duration +} + +// NewUIHandler creates a new UI handler with the given dependencies +func NewUIHandler(events events.EventManager, sseHandler *sse.SSEHandler, logger *zerolog.Logger) *UIHandler { + return &UIHandler{ + waitRegistry: NewWaitRegistry(30 * time.Second), // 30 second default timeout + events: events, + sseHandler: sseHandler, + logger: logger, + timeout: 30 * time.Second, + } +} + +// RegisterHandlers registers all UI-related HTTP handlers with the given mux +func (h *UIHandler) RegisterHandlers(mux *http.ServeMux) { + mux.Handle("/api/ui-update", h.handleUIUpdate()) + mux.Handle("/api/ui-action", h.handleUIAction()) +} +``` + +### 2. Create a Wait Registry Structure + +- [x] Define a `WaitRegistry` struct to track pending requests +- [x] Implement methods to register, resolve, and timeout requests +- [x] Use channels for synchronization between the update endpoint and action handler + +```go +// WaitRegistry tracks pending UI update requests waiting for user actions +type WaitRegistry struct { + pending map[string]chan UIActionResponse + mu sync.RWMutex + timeout time.Duration +} + +// UIActionResponse represents the response from a UI action +type UIActionResponse struct { + Action string + ComponentID string + Data map[string]interface{} + Error error + Timestamp time.Time +} + +// NewWaitRegistry creates a new registry with the specified timeout +func NewWaitRegistry(timeout time.Duration) *WaitRegistry { + return &WaitRegistry{ + pending: make(map[string]chan UIActionResponse), + timeout: timeout, + } +} + +// Register adds a new request to the registry and returns a channel for the response +func (wr *WaitRegistry) Register(requestID string) chan UIActionResponse { + wr.mu.Lock() + defer wr.mu.Unlock() + + responseChan := make(chan UIActionResponse, 1) + wr.pending[requestID] = responseChan + + return responseChan +} + +// Resolve completes a pending request with the given action response +func (wr *WaitRegistry) Resolve(requestID string, response UIActionResponse) bool { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + ch <- response + delete(wr.pending, requestID) + return true + } + return false +} + +// Cleanup removes a request from the registry +func (wr *WaitRegistry) Cleanup(requestID string) { + wr.mu.Lock() + defer wr.mu.Unlock() + + if ch, exists := wr.pending[requestID]; exists { + close(ch) + delete(wr.pending, requestID) + } +} + +// CleanupStale removes requests that have been in the registry longer than the timeout +func (wr *WaitRegistry) CleanupStale() int { + wr.mu.Lock() + defer wr.mu.Unlock() + + // This would require tracking timestamps for each request + // Implementation would depend on how we track request timestamps + return 0 +} +``` + +### 3. Update the Server Struct + +- [x] Remove UI-specific handlers from the Server struct +- [x] Add the UIHandler as a field in the Server struct +- [x] Update the Server initialization to create and use the UIHandler + +```go +type Server struct { + dir string + pages map[string]UIDefinition + routes map[string]http.HandlerFunc + watcher *watcher.Watcher + mux *http.ServeMux + mu sync.RWMutex + publisher message.Publisher + subscriber message.Subscriber + events events.EventManager + sseHandler *sse.SSEHandler + uiHandler *UIHandler // New field for UI handler +} + +func NewServer(dir string) (*Server, error) { + // ... existing initialization + + // Create SSE handler + sseHandler := sse.NewSSEHandler(eventManager, &log.Logger) + + // Create UI handler + uiHandler := NewUIHandler(eventManager, sseHandler, &log.Logger) + + s := &Server{ + dir: dir, + pages: make(map[string]UIDefinition), + routes: make(map[string]http.HandlerFunc), + mux: http.NewServeMux(), + publisher: publisher, + subscriber: publisher, + events: eventManager, + sseHandler: sseHandler, + uiHandler: uiHandler, + } + + // Register component renderers + s.registerComponentRenderers() + + // ... rest of initialization + + // Set up SSE endpoint + s.mux.Handle("/sse", sseHandler) + + // Set up static file handler + s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + // Register UI handlers + uiHandler.RegisterHandlers(s.mux) + + // ... rest of initialization + + return s, nil +} +``` + +### 4. Implement the UI Update Handler in UIHandler + +- [x] Move the UI update handler from Server to UIHandler +- [x] Generate a unique request ID for each update +- [x] Add the request ID to the UI definition sent to the frontend +- [x] Register the request in the wait registry +- [x] Wait for a response or timeout + +```go +func (h *UIHandler) handleUIUpdate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var jsonData map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&jsonData); err != nil { + // Return detailed JSON error response + h.sendErrorResponse(w, http.StatusBadRequest, "json_parse_error", "Invalid JSON: "+err.Error(), nil) + return + } + + // Generate a unique request ID + requestID := uuid.New().String() + + // Add the request ID to the UI definition + // This will be used by the frontend to associate actions with this request + jsonData["requestID"] = requestID + + // Convert to YAML for storage + yamlData, err := yaml.Marshal(jsonData) + if err != nil { + h.sendErrorResponse(w, http.StatusInternalServerError, "yaml_conversion_error", "Failed to convert to YAML: "+err.Error(), nil) + return + } + + // Parse into UIDefinition + var def UIDefinition + if err := yaml.Unmarshal(yamlData, &def); err != nil { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_definition_error", "Invalid UI definition: "+err.Error(), map[string]interface{}{ + "yaml": string(yamlData), + }) + return + } + + // Validate the UI definition + validationErrors := h.validateUIDefinition(def) + if len(validationErrors) > 0 { + h.sendErrorResponse(w, http.StatusBadRequest, "ui_validation_error", "UI definition validation failed", map[string]interface{}{ + "details": validationErrors, + }) + return + } + + // Register this request in the wait registry + responseChan := h.waitRegistry.Register(requestID) + + // Create and publish refresh-ui event with the request ID + event := events.UIEvent{ + Type: "refresh-ui", + PageID: "ui-update", + Component: def, + } + + // Add the request ID to the event data if possible + if compData, ok := event.Component.(UIDefinition); ok { + compData.RequestID = requestID + event.Component = compData + } + + if err := h.events.Publish("ui-update", event); err != nil { + // Clean up the registry entry + h.waitRegistry.Cleanup(requestID) + h.sendErrorResponse(w, http.StatusInternalServerError, "event_publish_error", "Failed to publish event: "+err.Error(), nil) + return + } + + // Log that we're waiting for a response + h.logger.Debug(). + Str("requestId", requestID). + Msg("Waiting for UI action response") + + // Wait for the response or timeout + select { + case response := <-responseChan: + // Request was resolved with a user action + if response.Error != nil { + // There was an error processing the action + h.sendErrorResponse(w, http.StatusInternalServerError, "action_processing_error", response.Error.Error(), nil) + return + } + + // Log the successful response + h.logger.Info(). + Str("requestId", requestID). + Str("action", response.Action). + Str("componentId", response.ComponentID). + Msg("UI action received, completing request") + + // Return success with the action data + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "success", + "action": response.Action, + "componentId": response.ComponentID, + "data": response.Data, + }) + + case <-time.After(h.timeout): + // Request timed out + h.waitRegistry.Cleanup(requestID) + + h.logger.Warn(). + Str("requestId", requestID). + Dur("timeout", h.timeout). + Msg("Request timed out waiting for UI action") + + h.sendErrorResponse(w, http.StatusRequestTimeout, "timeout", "No user action received within the timeout period", nil) + } + } +} +``` + +### 5. Implement the UI Action Handler in UIHandler + +- [x] Move the UI action handler from Server to UIHandler +- [x] Update to check for request IDs +- [x] Resolve waiting requests when matching actions are received + +```go +func (h *UIHandler) handleUIAction() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Only accept POST requests + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse JSON body + var action struct { + ComponentID string `json:"componentId"` + Action string `json:"action"` + Data map[string]interface{} `json:"data"` + RequestID string `json:"requestId"` // Added field for request ID + } + + if err := json.NewDecoder(r.Body).Decode(&action); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + // Determine if this is an important event to log at INFO level + isImportantEvent := false + switch action.Action { + case "clicked", "changed", "submitted": + isImportantEvent = true + } + + // Log the action at appropriate level + logger := h.logger.Debug() + if isImportantEvent { + logger = h.logger.Info() + } + + // Create log entry with component and action info + logger = logger. + Str("componentId", action.ComponentID). + Str("action", action.Action) + + // Add request ID to log if present + if action.RequestID != "" { + logger = logger.Str("requestId", action.RequestID) + } + + // Add data to log if it exists and is relevant + if len(action.Data) > 0 { + // For form submissions, log the form data in detail + if action.Action == "submitted" && action.Data["formData"] != nil { + logger = logger.Interface("formData", action.Data["formData"]) + } else if action.Action == "changed" { + // For changed events, log the new value + logger = logger.Interface("data", action.Data) + } else { + // For other events, just log that data exists + logger = logger.Bool("hasData", true) + } + } + + // Output the log message + logger.Msg("UI action received") + + // Check if this action is associated with a waiting request + if action.RequestID != "" && (action.Action == "clicked" || action.Action == "submitted") { + // Try to resolve the waiting request + resolved := h.waitRegistry.Resolve(action.RequestID, UIActionResponse{ + Action: action.Action, + ComponentID: action.ComponentID, + Data: action.Data, + Error: nil, + Timestamp: time.Now(), + }) + + if resolved { + logger.Bool("waitingRequestResolved", true).Msg("Resolved waiting request") + } + } + + // Return success response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + if err != nil { + http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) + } + } +} +``` + +### 6. Move UI Update Page Handler to UIHandler + +- [x] Move the UI update page handler from Server to UIHandler + + +### 7. Move UI Definition Validation to UIHandler + +- [x] Move the validation logic from Server to UIHandler + +```go +// validateUIDefinition checks a UI definition for common errors +func (h *UIHandler) validateUIDefinition(def UIDefinition) []map[string]interface{} { + var errors []map[string]interface{} + + // Check if components exist + if len(def.Components) == 0 { + errors = append(errors, map[string]interface{}{ + "path": "components", + "message": "No components defined", + }) + return errors + } + + // Validate each component + for i, comp := range def.Components { + for typ, props := range comp { + propsMap, ok := props.(map[string]interface{}) + if !ok { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component properties must be a map", + }) + continue + } + + // Check for required ID + if _, hasID := propsMap["id"]; !hasID && h.requiresID(typ) { + errors = append(errors, map[string]interface{}{ + "path": fmt.Sprintf("components[%d].%s", i, typ), + "message": "Component is missing required 'id' property", + }) + } + + // Validate nested components in forms + if typ == "form" { + // ... existing validation logic for forms + } + + // Validate list items + if typ == "list" { + // ... existing validation logic for lists + } + } + } + + return errors +} + +// requiresID returns true if the component type requires an ID +func (h *UIHandler) requiresID(componentType string) bool { + switch componentType { + case "text", "title": + // These can optionally have IDs + return false + default: + // All other components require IDs + return true + } +} +``` + +### 8. Update the UIEvent Structure + +- [x] Add RequestID field to the UIEvent struct + +```go +// In pkg/events/types.go +type UIEvent struct { + Type string `json:"type"` // e.g. "component-update", "page-reload" + PageID string `json:"pageId"` // Which page is being updated + Component interface{} `json:"component"` // The updated component data + RequestID string `json:"requestId,omitempty"` // Optional request ID for tracking user actions +} +``` + +### 9. Add Cleanup Mechanism for Orphaned Requests + +- [x] Implement a background goroutine to clean up stale requests + +```go +// Add to UIHandler initialization +func NewUIHandler(events events.EventManager, sseHandler *sse.SSEHandler, logger *zerolog.Logger) *UIHandler { + h := &UIHandler{ + waitRegistry: NewWaitRegistry(30 * time.Second), + events: events, + sseHandler: sseHandler, + logger: logger, + timeout: 30 * time.Second, + } + + // Start background cleanup for orphaned requests + go h.cleanupOrphanedRequests(context.Background()) + + return h +} + +// Add cleanup function +func (h *UIHandler) cleanupOrphanedRequests(ctx context.Context) { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Perform cleanup + count := h.waitRegistry.CleanupStale() + if count > 0 { + h.logger.Debug().Int("count", count).Msg("Cleaned up orphaned requests") + } + + case <-ctx.Done(): + return + } + } +} +``` + +### 10. Modify the Frontend JavaScript to Include Request ID + +- [x] Update the client-side code to extract and include the request ID in action events + +```javascript +// Add to the page template or UI update JavaScript +function sendUIAction(componentId, action, data = {}) { + // Get the request ID from the page if it exists + const requestId = document.body.getAttribute('data-request-id') || + document.querySelector('[data-request-id]')?.getAttribute('data-request-id') || + ''; + + logToConsole(`Component ${componentId} ${action}${requestId ? ' (request: ' + requestId + ')' : ''}`); + + // Rest of the existing function... + + // Include the request ID in the action data + fetch('/api/ui-action', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + componentId: componentId, + action: action, + data: data, + requestId: requestId // Include the request ID + }) + }) + // Rest of the existing function... +} +``` + +### 11. Update the Page Template to Include Request ID + +- [x] Modify the page template to include the request ID as a data attribute + +```go +// In pageTemplate or pageContentTemplate +func pageContentTemplate(pageID string, def UIDefinition) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + // Extract request ID if present + requestID := extractRequestID(def) + + // Include the request ID as a data attribute on the container + if requestID != "" { + fmt.Fprintf(w, `
`, requestID) + } else { + fmt.Fprintf(w, `
`) + } + + // Rest of the existing template... + + fmt.Fprintf(w, `
`) + return nil + }) +} + +// Helper function to extract request ID from UI definition +func extractRequestID(def UIDefinition) string { + // Try to extract from the component data + for _, comp := range def.Components { + for _, props := range comp { + if propsMap, ok := props.(map[string]interface{}); ok { + if rid, ok := propsMap["requestID"].(string); ok { + return rid + } + } + } + } + return "" +} +``` + +## Configuration Options + +- [ ] Make timeout configurable via environment variables or config file +- [ ] Add option to disable wait-for-response behavior for specific endpoints +- [ ] Configure logging verbosity for UI actions + +```go +type UIHandlerConfig struct { + Timeout time.Duration + CleanupInterval time.Duration + WaitForResponse bool + LogFormData bool + LogDebugEvents bool +} + +func DefaultUIHandlerConfig() UIHandlerConfig { + return UIHandlerConfig{ + Timeout: 30 * time.Second, + CleanupInterval: 5 * time.Minute, + WaitForResponse: true, + LogFormData: true, + LogDebugEvents: false, + } +} +``` + +## Testing Plan + +1. Test the basic flow: + - [ ] Send a UI update request + - [ ] Verify the UI is updated with the request ID + - [ ] Trigger a button click or form submission + - [ ] Verify the original request completes with the action data + +2. Test timeout handling: + - [ ] Send a UI update request + - [ ] Don't trigger any user action + - [ ] Verify the request times out after the configured duration + +3. Test error handling: + - [ ] Test with invalid UI definitions + - [ ] Test with network errors during event publishing + - [ ] Test with concurrent requests for the same component + +4. Test the refactored code structure: + - [ ] Verify all UI handlers are properly registered + - [ ] Verify the Server struct no longer has UI-specific logic + - [ ] Verify the UIHandler properly encapsulates all UI functionality + +## Implementation Considerations + +1. **Performance**: The wait registry should be efficient for concurrent requests +2. **Memory Management**: Ensure proper cleanup of channels and registry entries +3. **Error Handling**: Provide clear error messages for all failure scenarios +4. **Timeout Configuration**: Make the timeout configurable via server options +5. **Logging**: Add detailed logging for debugging and monitoring +6. **Code Organization**: Keep the UIHandler focused on UI concerns only +7. **Testability**: Design the code to be easily testable with mocks + +## Benefits of the Refactoring + +1. **Separation of Concerns**: UI handling logic is separated from the core server functionality +2. **Improved Maintainability**: Smaller, focused structs are easier to understand and maintain +3. **Better Testability**: UI handlers can be tested independently from the server +4. **Reduced Complexity**: The Server struct is simplified by removing UI-specific logic +5. **Extensibility**: New UI features can be added to the UIHandler without modifying the Server + +## Conclusion + +This implementation will create a synchronous flow between UI updates and user actions, allowing the server to wait for user interaction before completing the request. The use of channels and a wait registry provides a clean way to handle the asynchronous nature of user interactions while maintaining a synchronous API for the caller. + +Additionally, the refactoring to move UI handlers into their own struct will significantly improve the code organization and maintainability of the codebase. \ No newline at end of file diff --git a/ttmp/2025-02-22/changelog.md b/ttmp/2025-02-22/changelog.md index 249d53b..443e8c9 100644 --- a/ttmp/2025-02-22/changelog.md +++ b/ttmp/2025-02-22/changelog.md @@ -28,4 +28,17 @@ Created comprehensive documentation to help new developers understand the SSE an - Explained how templates are selected and rendered - Described how page data is managed and passed through the system - Included code examples and a complete walkthrough of the update process -- Created `ttmp/2025-02-22/03-sse-page-handling.md` as a developer guide \ No newline at end of file +- Created `ttmp/2025-02-22/03-sse-page-handling.md` as a developer guide + +# UI Action Handler with Wait-for-Response + +Added a new UI handler implementation that waits for user actions before completing requests. This creates a synchronous flow between UI updates and user interactions, making it easier to build interactive applications. + +- Created a new `UIHandler` struct to encapsulate all UI-related functionality +- Implemented a `WaitRegistry` to track pending requests using channels +- Added request ID generation and tracking between UI updates and actions +- Implemented timeout handling for requests without responses +- Integrated the UIHandler with the main server by removing UI-specific handlers from the Server struct +- Updated the Server initialization to create and use the UIHandler +- Added RequestID field to the UIEvent struct for better tracking of user interactions +- Implemented background cleanup for orphaned requests \ No newline at end of file