diff --git a/README.md b/README.md index 87e2fcf..53e148f 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,6 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode ## TODO -- proper loading screen -- preload icons - multiple chats ## Built With diff --git a/main.go b/main.go index 7f772f0..2804d23 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "io/fs" "net/http" "path/filepath" "strings" @@ -18,6 +19,9 @@ var log = logger.New().DetectTerminal().WithOptions(logger.Options{ }) func main() { + icons, err := LoadIcons() + log.MustPanic(err) + models, err := LoadModels() log.MustPanic(err) @@ -35,6 +39,7 @@ func main() { "authentication": env.Authentication.Enabled, "authenticated": IsAuthenticated(r), "search": env.Tokens.Exa != "", + "icons": icons, "models": models, "prompts": Prompts, "version": Version, @@ -66,3 +71,32 @@ func cache(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func LoadIcons() ([]string, error) { + var icons []string + + directory := filepath.Join("static", "css", "icons") + + err := filepath.Walk(directory, func(path string, info fs.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + + if strings.HasSuffix(path, ".svg") { + rel, err := filepath.Rel(directory, path) + if err != nil { + return err + } + + icons = append(icons, filepath.ToSlash(rel)) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return icons, nil +} diff --git a/static/css/chat.css b/static/css/chat.css index ac6512e..5c053bc 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -91,6 +91,40 @@ body.loading #version { top: 6px; } +#loading { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + backdrop-filter: blur(10px); + transition: opacity 250ms; +} + +#loading .inner { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + gap: 10px; + align-items: center; + font-weight: 500; + font-size: 22px; + line-height: 22px; +} + +#loading img { + width: 50px; + animation: wiggling 750ms ease-in-out infinite; +} + +body:not(.loading) #loading { + opacity: 0; + pointer-events: none; +} + #page { display: flex; flex-direction: column; @@ -664,6 +698,7 @@ select { } body.loading #version, +#loading .inner::after, .modal.loading .content::after, .reasoning .toggle::before, .reasoning .toggle::after, @@ -992,6 +1027,20 @@ label[for="reasoning-tokens"] { background: #89bb77; } +@keyframes wiggling { + 0% { + transform: translate(0px); + } + + 50% { + transform: translate(-10px, 0px); + } + + 100% { + transform: translate(0px); + } +} + @keyframes rotating { from { transform: rotate(0deg); diff --git a/static/index.html b/static/index.html index f36f378..d113756 100644 --- a/static/index.html +++ b/static/index.html @@ -18,6 +18,12 @@
+