diff --git a/cmd/prompto/cmds/serve.go b/cmd/prompto/cmds/serve.go index a106bee..bc12b2f 100644 --- a/cmd/prompto/cmds/serve.go +++ b/cmd/prompto/cmds/serve.go @@ -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) } diff --git a/go.mod b/go.mod index 83c583e..873fe89 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-go-golems/prompto -go 1.21 +go 1.22 toolchain go1.23.3 @@ -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 diff --git a/go.sum b/go.sum index a8dc2f3..4383a7c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/server/handlers.go b/pkg/server/handlers.go deleted file mode 100644 index 155affd..0000000 --- a/pkg/server/handlers.go +++ /dev/null @@ -1,101 +0,0 @@ -package server - -import ( - _ "embed" - "html/template" - "net/http" - "strings" - - "github.com/go-go-golems/prompto/pkg" -) - -//go:embed static/js/favorites.js -var favoritesJS string - -//go:embed static/templates/root.html -var rootTemplate string - -//go:embed static/templates/repoList.html -var repoListTemplate string - -func rootHandler(state *ServerState) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - - tmpl, err := state.CreateTemplateWithFuncs("root", rootTemplate+repoListTemplate) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - data := struct { - Groups []string - FavoritesJS template.JS - }{ - Groups: state.GetAllGroups(), - FavoritesJS: template.JS(favoritesJS), - } - - w.Header().Set("Content-Type", "text/html") - err = tmpl.Execute(w, data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } -} - -func searchHandler(state *ServerState) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - query := r.FormValue("search") - results := make(map[string][]pkg.Prompto) - - state.mu.RLock() - for _, file := range state.GetAllPromptos() { - if strings.Contains(strings.ToLower(file.Name), strings.ToLower(query)) { - group := strings.SplitN(file.Name, "/", 2)[0] - results[group] = append(results[group], file) - } - } - state.mu.RUnlock() - - groups := make([]string, 0) - for group := range results { - groups = append(groups, group) - } - - funcMap := template.FuncMap{ - "PromptosByGroup": func(group string) []pkg.Prompto { - return results[group] - }, - } - - tmpl, err := state.CreateTemplateWithFuncs("repoList", repoListTemplate) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - tmpl = tmpl.Funcs(funcMap) - - data := struct { - Groups []string - }{ - Groups: groups, - } - - w.Header().Set("Content-Type", "text/html") - err = tmpl.ExecuteTemplate(w, "repoList", data) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } -} diff --git a/pkg/server/handlers/handlers.go b/pkg/server/handlers/handlers.go new file mode 100644 index 0000000..eb2b489 --- /dev/null +++ b/pkg/server/handlers/handlers.go @@ -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, + } +} diff --git a/pkg/server/handlers/index.go b/pkg/server/handlers/index.go new file mode 100644 index 0000000..9395e36 --- /dev/null +++ b/pkg/server/handlers/index.go @@ -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) + } + } +} diff --git a/pkg/server/handlers/prompt.go b/pkg/server/handlers/prompt.go new file mode 100644 index 0000000..6b2f823 --- /dev/null +++ b/pkg/server/handlers/prompt.go @@ -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) + } + } +} diff --git a/pkg/server/refresh_handler.go b/pkg/server/handlers/refresh.go similarity index 75% rename from pkg/server/refresh_handler.go rename to pkg/server/handlers/refresh.go index 7f9db9b..8c7887e 100644 --- a/pkg/server/refresh_handler.go +++ b/pkg/server/handlers/refresh.go @@ -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 } diff --git a/pkg/server/repositories_handler.go b/pkg/server/handlers/repositories.go similarity index 67% rename from pkg/server/repositories_handler.go rename to pkg/server/handlers/repositories.go index ddf68b9..aa816a6 100644 --- a/pkg/server/repositories_handler.go +++ b/pkg/server/handlers/repositories.go @@ -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) diff --git a/pkg/server/prompt_handler.go b/pkg/server/prompt_handler.go deleted file mode 100644 index 6f36676..0000000 --- a/pkg/server/prompt_handler.go +++ /dev/null @@ -1,89 +0,0 @@ -package server - -import ( - "fmt" - "html/template" - "net/http" - "strings" - - "github.com/go-go-golems/prompto/pkg" -) - -func promptHandler(state *ServerState) http.HandlerFunc { - listTmpl := template.Must(template.New("promptList").Parse(` - - `)) - - return func(w http.ResponseWriter, r *http.Request) { - path := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/prompts/"), "/") - parts := strings.SplitN(path, "/", 2) - - if len(parts) == 1 { - // Directory listing - group := parts[0] - state.mu.RLock() - files := state.GetPromptosByGroup(group) - state.mu.RUnlock() - - w.Header().Set("Content-Type", "text/html") - err := listTmpl.Execute(w, files) - if err != nil { - http.Error(w, "Error rendering template", http.StatusInternalServerError) - return - } - return - } - - if len(parts) != 2 { - http.Error(w, "Invalid path", http.StatusBadRequest) - return - } - - group := parts[0] - promptName := path - - state.mu.RLock() - files := state.GetPromptosByGroup(group) - state.mu.RUnlock() - - var foundFile pkg.Prompto - for _, file := range files { - if file.Name == promptName { - foundFile = file - break - } - } - - if foundFile.Name == "" { - http.Error(w, "Prompt not found", http.StatusNotFound) - return - } - - // 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 { - http.Error(w, "Error rendering prompt", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/markdown") - _, _ = w.Write([]byte(content)) - } -} diff --git a/pkg/server/serve.go b/pkg/server/serve.go index f97bbc1..b3cf5a4 100644 --- a/pkg/server/serve.go +++ b/pkg/server/serve.go @@ -3,184 +3,15 @@ package server import ( "context" "fmt" - "html/template" "net/http" - "path/filepath" - "sort" - "sync" - "github.com/go-go-golems/clay/pkg/watcher" - "github.com/go-go-golems/glazed/pkg/helpers/templating" - "github.com/go-go-golems/prompto/pkg" - "github.com/rs/zerolog/log" - "github.com/spf13/viper" + "github.com/go-go-golems/prompto/pkg/server/handlers" + "github.com/go-go-golems/prompto/pkg/server/state" ) -type ServerState struct { - Repositories []string - Repos map[string]*pkg.Repository - mu sync.RWMutex - Watching bool -} - -func NewServerState(watching bool) *ServerState { - return &ServerState{ - Repos: make(map[string]*pkg.Repository), - Watching: watching, - } -} - -func (s *ServerState) LoadRepositories() error { - s.mu.Lock() - defer s.mu.Unlock() - - s.Repositories = viper.GetStringSlice("repositories") - for _, repoPath := range s.Repositories { - repo := pkg.NewRepository(repoPath) - err := repo.LoadPromptos() - if err != nil { - return fmt.Errorf("error loading files from repository %s: %w", repoPath, err) - } - s.Repos[repoPath] = repo - } - return nil -} - -func (s *ServerState) CreateTemplateWithFuncs(name, tmpl string) (*template.Template, error) { - funcMap := template.FuncMap{ - "PromptosByGroup": func(group string) []pkg.Prompto { - return s.GetPromptosByGroup(group) - }, - "AllRepositories": func() []string { - return s.GetAllRepositories() - }, - "AllPromptos": func() []pkg.Prompto { - return s.GetAllPromptos() - }, - "AllGroups": func() []string { - return s.GetAllGroups() - }, - "PromptosByRepository": func(repo string) []pkg.Prompto { - return s.Repos[repo].GetPromptos() - }, - "GroupsByRepository": func(repo string) []string { - return s.GetGroupsByRepository(repo) - }, - "PromptosForRepositoryAndGroup": func(repo, group string) []pkg.Prompto { - return s.GetPromptosForRepositoryAndGroup(repo, group) - }, - } - - return templating.CreateHTMLTemplate(name). - Funcs(funcMap). - Parse(tmpl) -} - -func (s *ServerState) GetAllRepositories() []string { - s.mu.RLock() - defer s.mu.RUnlock() - return s.Repositories -} - -func (s *ServerState) GetAllPromptos() []pkg.Prompto { - s.mu.RLock() - defer s.mu.RUnlock() - - var allPromptos []pkg.Prompto - for _, repo := range s.Repos { - allPromptos = append(allPromptos, repo.GetPromptos()...) - } - - sort.Slice(allPromptos, func(i, j int) bool { - return allPromptos[i].Name < allPromptos[j].Name - }) - - return allPromptos -} - -func (s *ServerState) GetAllGroups() []string { - s.mu.RLock() - defer s.mu.RUnlock() - - groupSet := make(map[string]struct{}) - for _, repo := range s.Repos { - for _, group := range repo.GetGroups() { - groupSet[group] = struct{}{} - } - } - - var groups []string - for group := range groupSet { - groups = append(groups, group) - } - - sort.Strings(groups) - return groups -} - -func (s *ServerState) GetPromptosByGroup(group string) []pkg.Prompto { - s.mu.RLock() - defer s.mu.RUnlock() - - var groupPromptos []pkg.Prompto - for _, repo := range s.Repos { - groupPromptos = append(groupPromptos, repo.GetPromptosByGroup(group)...) - } - - sort.Slice(groupPromptos, func(i, j int) bool { - return groupPromptos[i].Name < groupPromptos[j].Name - }) - - return groupPromptos -} - -func (s *ServerState) GetGroupsByRepository(repo string) []string { - s.mu.RLock() - defer s.mu.RUnlock() - return s.Repos[repo].GetGroups() -} - -func (s *ServerState) GetPromptosForRepositoryAndGroup(repo, group string) []pkg.Prompto { - s.mu.RLock() - defer s.mu.RUnlock() - return s.Repos[repo].GetPromptosByGroup(group) -} - -func (s *ServerState) WatchRepositories(ctx context.Context) error { - if !s.Watching { - return nil - } - - for repoPath, repo := range s.Repos { - options := []watcher.Option{ - watcher.WithWriteCallback(func(path string) error { - log.Info().Msgf("File %s changed, reloading...", path) - s.mu.Lock() - defer s.mu.Unlock() - return repo.AddPrompto(path) - }), - watcher.WithRemoveCallback(func(path string) error { - log.Info().Msgf("File %s removed, removing from repository...", path) - s.mu.Lock() - defer s.mu.Unlock() - return repo.RemovePrompto(path) - }), - watcher.WithPaths(filepath.Join(repoPath, "prompto")), - } - - w := watcher.NewWatcher(options...) - go func() { - if err := w.Run(ctx); err != nil { - log.Error().Err(err).Msg("Watcher error") - } - }() - } - - return nil -} - -func Serve(port int, watching bool) error { - state := NewServerState(watching) +func Serve(port int, watching bool, repositories []string) error { + state := state.NewServerState(watching) + state.Repositories = repositories if err := state.LoadRepositories(); err != nil { return fmt.Errorf("error loading repositories: %w", err) } @@ -195,19 +26,20 @@ func Serve(port int, watching bool) error { } } - http.HandleFunc("/", logHandler(rootHandler(state))) - http.HandleFunc("/prompts/", logHandler(promptHandler(state))) - http.HandleFunc("/search", logHandler(searchHandler(state))) - http.HandleFunc("/refresh", logHandler(refreshHandler(state))) - http.HandleFunc("/repositories", logHandler(repositoriesHandler(state))) + h := handlers.NewHandlers(state) - fmt.Printf("Server is running on http://localhost:%d\n", port) - return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) -} + // Set up routes + mux := http.NewServeMux() + mux.Handle("/", h.Index()) + mux.Handle("/prompts/{name...}", h.PromptContent()) + mux.Handle("/search", h.Search()) + mux.Handle("/refresh", h.Refresh()) + mux.Handle("/repositories", h.Repositories()) -func logHandler(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s", r.Method, r.URL.Path) - next(w, r) - } + // Serve static files + fs := http.FileServer(http.Dir("pkg/server/static")) + mux.Handle("/static/", http.StripPrefix("/static/", fs)) + + fmt.Printf("Server is running on http://localhost:%d\n", port) + return http.ListenAndServe(fmt.Sprintf(":%d", port), mux) } diff --git a/pkg/server/state/state.go b/pkg/server/state/state.go new file mode 100644 index 0000000..b26884b --- /dev/null +++ b/pkg/server/state/state.go @@ -0,0 +1,177 @@ +package state + +import ( + "context" + "fmt" + "html/template" + "path/filepath" + "sort" + "sync" + + "github.com/go-go-golems/clay/pkg/watcher" + "github.com/go-go-golems/glazed/pkg/helpers/templating" + "github.com/go-go-golems/prompto/pkg" + "github.com/rs/zerolog/log" +) + +type ServerState struct { + Repositories []string + Repos map[string]*pkg.Repository + mu sync.RWMutex + Watching bool +} + +func NewServerState(watching bool) *ServerState { + return &ServerState{ + Repos: make(map[string]*pkg.Repository), + Watching: watching, + } +} + +func (s *ServerState) LoadRepositories() error { + s.mu.Lock() + defer s.mu.Unlock() + + for _, repoPath := range s.Repositories { + repo := pkg.NewRepository(repoPath) + err := repo.LoadPromptos() + if err != nil { + return fmt.Errorf("error loading files from repository %s: %w", repoPath, err) + } + s.Repos[repoPath] = repo + } + return nil +} + +func (s *ServerState) CreateTemplateWithFuncs(name, tmpl string) (*template.Template, error) { + funcMap := template.FuncMap{ + "PromptosByGroup": func(group string) []pkg.Prompto { + return s.GetPromptosByGroup(group) + }, + "AllRepositories": func() []string { + return s.GetAllRepositories() + }, + "AllPromptos": func() []pkg.Prompto { + return s.GetAllPromptos() + }, + "AllGroups": func() []string { + return s.GetAllGroups() + }, + "PromptosByRepository": func(repo string) []pkg.Prompto { + return s.Repos[repo].GetPromptos() + }, + "GroupsByRepository": func(repo string) []string { + return s.GetGroupsByRepository(repo) + }, + "PromptosForRepositoryAndGroup": func(repo, group string) []pkg.Prompto { + return s.GetPromptosForRepositoryAndGroup(repo, group) + }, + } + + return templating.CreateHTMLTemplate(name). + Funcs(funcMap). + Parse(tmpl) +} + +func (s *ServerState) GetAllRepositories() []string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Repositories +} + +func (s *ServerState) GetAllPromptos() []pkg.Prompto { + s.mu.RLock() + defer s.mu.RUnlock() + + var allPromptos []pkg.Prompto + for _, repo := range s.Repos { + allPromptos = append(allPromptos, repo.GetPromptos()...) + } + + sort.Slice(allPromptos, func(i, j int) bool { + return allPromptos[i].Name < allPromptos[j].Name + }) + + return allPromptos +} + +func (s *ServerState) GetAllGroups() []string { + s.mu.RLock() + defer s.mu.RUnlock() + + groupSet := make(map[string]struct{}) + for _, repo := range s.Repos { + for _, group := range repo.GetGroups() { + groupSet[group] = struct{}{} + } + } + + var groups []string + for group := range groupSet { + groups = append(groups, group) + } + + sort.Strings(groups) + return groups +} + +func (s *ServerState) GetPromptosByGroup(group string) []pkg.Prompto { + s.mu.RLock() + defer s.mu.RUnlock() + + var groupPromptos []pkg.Prompto + for _, repo := range s.Repos { + groupPromptos = append(groupPromptos, repo.GetPromptosByGroup(group)...) + } + + sort.Slice(groupPromptos, func(i, j int) bool { + return groupPromptos[i].Name < groupPromptos[j].Name + }) + + return groupPromptos +} + +func (s *ServerState) GetGroupsByRepository(repo string) []string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Repos[repo].GetGroups() +} + +func (s *ServerState) GetPromptosForRepositoryAndGroup(repo, group string) []pkg.Prompto { + s.mu.RLock() + defer s.mu.RUnlock() + return s.Repos[repo].GetPromptosByGroup(group) +} + +func (s *ServerState) WatchRepositories(ctx context.Context) error { + if !s.Watching { + return nil + } + + for repoPath, repo := range s.Repos { + options := []watcher.Option{ + watcher.WithWriteCallback(func(path string) error { + log.Info().Msgf("File %s changed, reloading...", path) + s.mu.Lock() + defer s.mu.Unlock() + return repo.AddPrompto(path) + }), + watcher.WithRemoveCallback(func(path string) error { + log.Info().Msgf("File %s removed, removing from repository...", path) + s.mu.Lock() + defer s.mu.Unlock() + return repo.RemovePrompto(path) + }), + watcher.WithPaths(filepath.Join(repoPath, "prompto")), + } + + w := watcher.NewWatcher(options...) + go func() { + if err := w.Run(ctx); err != nil { + log.Error().Err(err).Msg("Watcher error") + } + }() + } + + return nil +} diff --git a/pkg/server/static/js/favorites.js b/pkg/server/static/js/favorites.js index aad24a0..a795617 100644 --- a/pkg/server/static/js/favorites.js +++ b/pkg/server/static/js/favorites.js @@ -1,36 +1,70 @@ -// static/js/favorites.js -let favorites = []; +// Utility functions for managing favorites +function initFavorites() { + if (!localStorage.getItem('favorites')) { + localStorage.setItem('favorites', JSON.stringify([])); + } +} -function addToFavorites(promptName) { - if (!favorites.includes(promptName)) { - favorites.push(promptName); +function getFavorites() { + return JSON.parse(localStorage.getItem('favorites') || '[]'); +} + +function copyToClipboard(text) { + fetch("/prompts/" + text) + .then(response => response.text()) + .then(content => { + navigator.clipboard.writeText(content).then(() => { + const toastEl = document.getElementById('copyToast'); + const toast = new bootstrap.Toast(toastEl); + toast.show(); + }); + }); +} + +function addToFavorites(name) { + const favorites = getFavorites(); + if (!favorites.includes(name)) { + favorites.push(name); + localStorage.setItem('favorites', JSON.stringify(favorites)); renderFavorites(); + + const toastEl = document.getElementById('favToast'); + const toast = new bootstrap.Toast(toastEl); + toast.show(); } } -function removeFromFavorites(promptName) { - favorites = favorites.filter(fav => fav !== promptName); +function removeFromFavorites(name) { + const favorites = getFavorites(); + const newFavorites = favorites.filter(fav => fav !== name); + localStorage.setItem('favorites', JSON.stringify(newFavorites)); renderFavorites(); } function renderFavorites() { + const favorites = getFavorites(); const favoritesList = document.getElementById('favorites-list'); - favoritesList.innerHTML = ''; - favorites.forEach(fav => { - const li = document.createElement('li'); - li.innerHTML = ` - ${fav} - 📋 - - - `; - favoritesList.appendChild(li); - }); + if (!favoritesList) return; + + favoritesList.innerHTML = favorites.length === 0 + ? '

No favorites yet

' + : favorites.map(fav => ` +
+ ${fav} +
+ + +
+
+ `).join(''); } -function copyToClipboard(text) { - navigator.clipboard.writeText(text).then(function() { - alert('Copied to clipboard'); - }, function(err) { - alert('Failed to copy: ', err); - }); -} \ No newline at end of file +// Initialize favorites when the DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + initFavorites(); + renderFavorites(); +}); \ No newline at end of file diff --git a/pkg/server/static/templates/repoList.html b/pkg/server/static/templates/repoList.html deleted file mode 100644 index c32af21..0000000 --- a/pkg/server/static/templates/repoList.html +++ /dev/null @@ -1,14 +0,0 @@ -{{define "repoList"}} - {{range $group := .Groups}} -

{{.}}

- - {{end}} -{{end}} \ No newline at end of file diff --git a/pkg/server/static/templates/root.html b/pkg/server/static/templates/root.html deleted file mode 100644 index e25a684..0000000 --- a/pkg/server/static/templates/root.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - Prompto Repositories - - - - - - -
-

Prompto Repositories

-

Favorites

- - - Searching... -
- {{template "repoList" .}} -
-
- - \ No newline at end of file diff --git a/pkg/server/templates/components/prompt_list.templ b/pkg/server/templates/components/prompt_list.templ new file mode 100644 index 0000000..9b86e5c --- /dev/null +++ b/pkg/server/templates/components/prompt_list.templ @@ -0,0 +1,69 @@ +package components + +import "github.com/go-go-golems/prompto/pkg" + +script copyToClipboard(text string) { + copyToClipboard(text) +} + +script addToFavorites(name string) { + addToFavorites(name) +} + +templ PromptList(prompts []pkg.Prompto) { + for _, group := range getGroups(prompts) { +
+
+

{ group }

+
+
+ for _, prompt := range getPromptsByGroup(prompts, group) { +
+
+ + { prompt.Name } + +
+ + +
+
+
+ } +
+
+ } +} + +// Helper functions to group prompts +func getGroups(prompts []pkg.Prompto) []string { + groups := make(map[string]bool) + var result []string + for _, p := range prompts { + if !groups[p.Group] { + groups[p.Group] = true + result = append(result, p.Group) + } + } + return result +} + +func getPromptsByGroup(prompts []pkg.Prompto, group string) []pkg.Prompto { + var result []pkg.Prompto + for _, p := range prompts { + if p.Group == group { + result = append(result, p) + } + } + return result +} \ No newline at end of file diff --git a/pkg/server/templates/components/prompt_list_templ.go b/pkg/server/templates/components/prompt_list_templ.go new file mode 100644 index 0000000..48b7b10 --- /dev/null +++ b/pkg/server/templates/components/prompt_list_templ.go @@ -0,0 +1,166 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.793 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/go-go-golems/prompto/pkg" + +func copyToClipboard(text string) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_copyToClipboard_9315`, + Function: `function __templ_copyToClipboard_9315(text){copyToClipboard(text) +}`, + Call: templ.SafeScript(`__templ_copyToClipboard_9315`, text), + CallInline: templ.SafeScriptInline(`__templ_copyToClipboard_9315`, text), + } +} + +func addToFavorites(name string) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_addToFavorites_e022`, + Function: `function __templ_addToFavorites_e022(name){addToFavorites(name) +}`, + Call: templ.SafeScript(`__templ_addToFavorites_e022`, name), + CallInline: templ.SafeScriptInline(`__templ_addToFavorites_e022`, name), + } +} + +func PromptList(prompts []pkg.Prompto) 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + for _, group := range getGroups(prompts) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(group) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/server/templates/components/prompt_list.templ`, Line: 17, Col: 31} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, prompt := range getPromptsByGroup(prompts, group) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(prompt.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/server/templates/components/prompt_list.templ`, Line: 24, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, copyToClipboard(prompt.Name)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, addToFavorites(prompt.Name)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return templ_7745c5c3_Err + }) +} + +// Helper functions to group prompts +func getGroups(prompts []pkg.Prompto) []string { + groups := make(map[string]bool) + var result []string + for _, p := range prompts { + if !groups[p.Group] { + groups[p.Group] = true + result = append(result, p.Group) + } + } + return result +} + +func getPromptsByGroup(prompts []pkg.Prompto, group string) []pkg.Prompto { + var result []pkg.Prompto + for _, p := range prompts { + if p.Group == group { + result = append(result, p) + } + } + return result +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/server/templates/components/repository_list.templ b/pkg/server/templates/components/repository_list.templ new file mode 100644 index 0000000..c564244 --- /dev/null +++ b/pkg/server/templates/components/repository_list.templ @@ -0,0 +1,32 @@ +package components + +import "github.com/go-go-golems/prompto/pkg" + +templ RepositoryList(repositories []string, repos map[string]*pkg.Repository) { +
+ for _, repoPath := range repositories { +
+
+
+
{repoPath}
+ if repo, ok := repos[repoPath]; ok { +
+ for _, group := range repo.GetGroups() { + + } +
+ } +
+
+
+ } +
+} \ No newline at end of file diff --git a/pkg/server/templates/components/repository_list_templ.go b/pkg/server/templates/components/repository_list_templ.go new file mode 100644 index 0000000..f90d586 --- /dev/null +++ b/pkg/server/templates/components/repository_list_templ.go @@ -0,0 +1,111 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.793 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import "github.com/go-go-golems/prompto/pkg" + +func RepositoryList(repositories []string, repos map[string]*pkg.Repository) 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, repoPath := range repositories { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(repoPath) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/server/templates/components/repository_list.templ`, Line: 11, Col: 38} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if repo, ok := repos[repoPath]; ok { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, group := range repo.GetGroups() { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/server/templates/layout.templ b/pkg/server/templates/layout.templ new file mode 100644 index 0000000..8094298 --- /dev/null +++ b/pkg/server/templates/layout.templ @@ -0,0 +1,25 @@ +package templates + +templ Layout() { + + + + + + Prompto + + + + + + +
+
+

Prompto

+
+ { children... } +
+ + + +} \ No newline at end of file diff --git a/pkg/server/templates/layout_templ.go b/pkg/server/templates/layout_templ.go new file mode 100644 index 0000000..6cae231 --- /dev/null +++ b/pkg/server/templates/layout_templ.go @@ -0,0 +1,48 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.793 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func Layout() 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Prompto

Prompto

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/server/templates/pages/index.templ b/pkg/server/templates/pages/index.templ new file mode 100644 index 0000000..f4c72b4 --- /dev/null +++ b/pkg/server/templates/pages/index.templ @@ -0,0 +1,82 @@ +package pages + +import ( + "github.com/go-go-golems/prompto/pkg" + "github.com/go-go-golems/prompto/pkg/server/templates" + "github.com/go-go-golems/prompto/pkg/server/templates/components" +) + +script copyToClipboard(text string) { + copyToClipboard(text) +} + +script addToFavorites(name string) { + addToFavorites(name) +} + +templ Index(repositories []string, repos map[string]*pkg.Repository) { + @templates.Layout() { + +
+ + +
+
+
+
+
+ + + + +
+
+
+ for _, repo := range repositories { + @components.PromptList(repos[repo].GetPromptos()) + } +
+
+
+
+
+

Select a prompt to view its details

+
+
+
+
+
Favorites
+
+
+

No favorites yet

+
+
+
+
+ } +} diff --git a/pkg/server/templates/pages/index_templ.go b/pkg/server/templates/pages/index_templ.go new file mode 100644 index 0000000..f5a7abd --- /dev/null +++ b/pkg/server/templates/pages/index_templ.go @@ -0,0 +1,94 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.793 +package pages + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "github.com/go-go-golems/prompto/pkg" + "github.com/go-go-golems/prompto/pkg/server/templates" + "github.com/go-go-golems/prompto/pkg/server/templates/components" +) + +func copyToClipboard(text string) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_copyToClipboard_c982`, + Function: `function __templ_copyToClipboard_c982(text){copyToClipboard(text) +}`, + Call: templ.SafeScript(`__templ_copyToClipboard_c982`, text), + CallInline: templ.SafeScriptInline(`__templ_copyToClipboard_c982`, text), + } +} + +func addToFavorites(name string) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_addToFavorites_543a`, + Function: `function __templ_addToFavorites_543a(name){addToFavorites(name) +}`, + Call: templ.SafeScript(`__templ_addToFavorites_543a`, name), + CallInline: templ.SafeScriptInline(`__templ_addToFavorites_543a`, name), + } +} + +func Index(repositories []string, repos map[string]*pkg.Repository) 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 { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Var2 := 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 { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Prompt copied to clipboard!
Added to favorites!
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, repo := range repositories { + templ_7745c5c3_Err = components.PromptList(repos[repo].GetPromptos()).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Select a prompt to view its details

Favorites

No favorites yet

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = templates.Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate