Skip to content

Commit

Permalink
Merge pull request #24 from wesen/task/nicer-web-ui
Browse files Browse the repository at this point in the history
Modernize web UI with Bootstrap and HTMX
  • Loading branch information
wesen authored Dec 31, 2024
2 parents 095428a + 9116e96 commit 9e7d819
Show file tree
Hide file tree
Showing 23 changed files with 1,106 additions and 465 deletions.
2 changes: 1 addition & 1 deletion cmd/prompto/cmds/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ func (s *ServeCommand) run(cmd *cobra.Command, args []string) error {
port, _ := cmd.Flags().GetInt("port")
watching, _ := cmd.Flags().GetBool("watching")

return server.Serve(port, watching)
return server.Serve(port, watching, s.repositories)
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/go-go-golems/prompto

go 1.21
go 1.22

toolchain go1.23.3

Expand All @@ -19,6 +19,7 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/a-h/templ v0.2.793 // indirect
github.com/adrg/frontmatter v0.2.0 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/adrg/frontmatter v0.2.0 h1:/DgnNe82o03riBd1S+ZDjd43wAmC6W35q67NHeLkPd4=
github.com/adrg/frontmatter v0.2.0/go.mod h1:93rQCj3z3ZlwyxxpQioRKC1wDLto4aXHrbqIsnH9wmE=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
Expand Down
101 changes: 0 additions & 101 deletions pkg/server/handlers.go

This file was deleted.

15 changes: 15 additions & 0 deletions pkg/server/handlers/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package handlers

import (
"github.com/go-go-golems/prompto/pkg/server/state"
)

type Handlers struct {
state *state.ServerState
}

func NewHandlers(state *state.ServerState) *Handlers {
return &Handlers{
state: state,
}
}
22 changes: 22 additions & 0 deletions pkg/server/handlers/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package handlers

import (
"net/http"

"github.com/go-go-golems/prompto/pkg/server/templates/pages"
)

func (h *Handlers) Index() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}

component := pages.Index(h.state.GetAllRepositories(), h.state.Repos)
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
177 changes: 177 additions & 0 deletions pkg/server/handlers/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package handlers

import (
"fmt"
"net/http"
"strings"

"github.com/go-go-golems/prompto/pkg"
"github.com/go-go-golems/prompto/pkg/server/templates/components"
"github.com/rs/zerolog/log"
)

func (h *Handlers) PromptList() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 4 {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}

repo := parts[2]
group := parts[3]

repository, ok := h.state.Repos[repo]
if !ok {
http.Error(w, "Repository not found", http.StatusNotFound)
return
}

prompts := repository.GetPromptosByGroup(group)
component := components.PromptList(prompts)
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

func (h *Handlers) PromptContent() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
logger := log.With().Str("handler", "PromptContent").Logger()

name := r.PathValue("name")
logger.Debug().Str("path", name).Msg("handling prompt request")

if name == "" {
logger.Debug().Msg("invalid path: empty name")
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}

// Handle root directory listing
if name == "" {
logger.Debug().Msg("rendering root directory listing")
var allPrompts []pkg.Prompto
for _, repo := range h.state.Repos {
allPrompts = append(allPrompts, repo.GetPromptos()...)
}
component := components.PromptList(allPrompts)
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}

// Split the path into group and prompt path
parts := strings.SplitN(name, "/", 2)

// Handle group listing
if len(parts) == 1 {
group := parts[0]
logger = logger.With().Str("group", group).Logger()
logger.Debug().Msg("rendering group listing")

// Get all prompts from this group across all repositories
var prompts []pkg.Prompto
for _, repo := range h.state.Repos {
prompts = append(prompts, repo.GetPromptosByGroup(group)...)
}

component := components.PromptList(prompts)
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}

group := parts[0]
promptPath := name
logger = logger.With().
Str("group", group).
Str("promptPath", promptPath).
Logger()

logger.Debug().Msg("looking up prompt")

// Get all prompts for this group and find the matching one
files := h.state.GetPromptosByGroup(group)
var foundFile pkg.Prompto
for _, file := range files {
if file.Name == promptPath {
foundFile = file
break
}
}

if foundFile.Name == "" {
logger.Debug().Msg("prompt not found")
http.Error(w, "Prompt not found", http.StatusNotFound)
return
}

logger = logger.With().
Str("repository", foundFile.Repository).
Str("prompt", foundFile.Name).
Logger()
logger.Debug().Msg("found prompt, rendering with args")

// Extract URL parameters
queryParams := r.URL.Query()
var restArgs []string
for key, values := range queryParams {
for _, value := range values {
if value == "" {
// pass non-keyword arguments as a straight string
restArgs = append(restArgs, key)
} else {
restArgs = append(restArgs, fmt.Sprintf("--%s", key), value)
}
}
}

content, err := foundFile.Render(foundFile.Repository, restArgs)
if err != nil {
logger.Debug().Err(err).Msg("error rendering prompt")
http.Error(w, "Error rendering prompt", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "text/markdown")
_, _ = w.Write([]byte(content))
}
}

func (h *Handlers) Search() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
var matchingPrompts []pkg.Prompto

if query == "" {
// Show all prompts when query is empty
repoNames := h.state.GetAllRepositories()
for _, repoName := range repoNames {
repo := h.state.Repos[repoName]
matchingPrompts = append(matchingPrompts, repo.GetPromptos()...)
}
} else {
// Search for matching prompts
for _, repo := range h.state.Repos {
for _, prompt := range repo.GetPromptos() {
if strings.Contains(strings.ToLower(prompt.Name), strings.ToLower(query)) ||
strings.Contains(strings.ToLower(prompt.Group), strings.ToLower(query)) {
matchingPrompts = append(matchingPrompts, prompt)
}
}
}
}

component := components.PromptList(matchingPrompts)
err := component.Render(r.Context(), w)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package server
package handlers

import (
"net/http"
)

func refreshHandler(state *ServerState) http.HandlerFunc {
func (h *Handlers) Refresh() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}

if err := state.LoadRepositories(); err != nil {
if err := h.state.LoadRepositories(); err != nil {
http.Error(w, "Error refreshing repositories", http.StatusInternalServerError)
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package server
package handlers

import (
"encoding/json"
"net/http"
)

func repositoriesHandler(state *ServerState) http.HandlerFunc {
func (h *Handlers) Repositories() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
state.mu.RLock()
repos := state.Repositories
state.mu.RUnlock()
repos := h.state.GetAllRepositories()

w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(repos)
Expand Down
Loading

0 comments on commit 9e7d819

Please sign in to comment.