1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-09-09 09:19:54 +00:00

5 Commits

Author SHA1 Message Date
Laura
ebb27ef34e loading screen and icon preload 2025-08-19 17:12:36 +02:00
Laura
c24b0e87f7 example nginx config 2025-08-19 16:42:30 +02:00
fc27441bda todo 2025-08-18 05:21:24 +02:00
89df106aa6 todo 2025-08-18 05:16:18 +02:00
6bd6554997 config tweaks 2025-08-18 05:15:48 +02:00
7 changed files with 142 additions and 3 deletions

View File

@@ -77,6 +77,37 @@ authentication:
After a successful login, whiskr issues a signed (HMAC-SHA256) token, using the server secret (`tokens.secret` in `config.yml`). This is stored as a cookie and re-used for future authentications. After a successful login, whiskr issues a signed (HMAC-SHA256) token, using the server secret (`tokens.secret` in `config.yml`). This is stored as a cookie and re-used for future authentications.
## Nginx (optional)
When running behind a reverse proxy like nginx, you can have the proxy serve static files.
```ngnix
server {
listen 443 ssl;
server_name chat.example.com;
http2 on;
root /path/to/whiskr/static;
location / {
index index.html index.htm;
etag on;
add_header Cache-Control "public, max-age=2592000, must-revalidate";
expires 30d;
}
location ~ ^/- {
proxy_pass http://127.0.0.1:3443;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
}
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
```
## Usage ## Usage
- Send a message with `Ctrl+Enter` or the send button - Send a message with `Ctrl+Enter` or the send button

10
env.go
View File

@@ -41,7 +41,13 @@ type Environment struct {
Authentication EnvAuthentication `json:"authentication"` Authentication EnvAuthentication `json:"authentication"`
} }
var env Environment var env = Environment{
// defaults
Settings: EnvSettings{
CleanContent: true,
MaxIterations: 3,
},
}
func init() { func init() {
file, err := os.OpenFile("config.yml", os.O_RDONLY, 0) file, err := os.OpenFile("config.yml", os.O_RDONLY, 0)
@@ -119,7 +125,7 @@ func (e *Environment) Store() error {
"$.tokens.openrouter": {yaml.HeadComment(" openrouter.ai api token (required)")}, "$.tokens.openrouter": {yaml.HeadComment(" openrouter.ai api token (required)")},
"$.tokens.exa": {yaml.HeadComment(" exa search api token (optional; used by search tools)")}, "$.tokens.exa": {yaml.HeadComment(" exa search api token (optional; used by search tools)")},
"$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: false)")}, "$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")},
"$.settings.iterations": {yaml.HeadComment(" max model turns per request (optional; default: 3)")}, "$.settings.iterations": {yaml.HeadComment(" max model turns per request (optional; default: 3)")},
"$.authentication.enabled": {yaml.HeadComment(" require login with username and password")}, "$.authentication.enabled": {yaml.HeadComment(" require login with username and password")},

View File

@@ -10,7 +10,7 @@ tokens:
exa: "" exa: ""
settings: settings:
# normalize unicode in assistant output (optional; default: false) # normalize unicode in assistant output (optional; default: true)
cleanup: true cleanup: true
# max model turns per request (optional; default: 3) # max model turns per request (optional; default: 3)
iterations: 3 iterations: 3

34
main.go
View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"io/fs"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -18,6 +19,9 @@ var log = logger.New().DetectTerminal().WithOptions(logger.Options{
}) })
func main() { func main() {
icons, err := LoadIcons()
log.MustPanic(err)
models, err := LoadModels() models, err := LoadModels()
log.MustPanic(err) log.MustPanic(err)
@@ -35,6 +39,7 @@ func main() {
"authentication": env.Authentication.Enabled, "authentication": env.Authentication.Enabled,
"authenticated": IsAuthenticated(r), "authenticated": IsAuthenticated(r),
"search": env.Tokens.Exa != "", "search": env.Tokens.Exa != "",
"icons": icons,
"models": models, "models": models,
"prompts": Prompts, "prompts": Prompts,
"version": Version, "version": Version,
@@ -66,3 +71,32 @@ func cache(next http.Handler) http.Handler {
next.ServeHTTP(w, r) 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
}

View File

@@ -91,6 +91,40 @@ body.loading #version {
top: 6px; 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 { #page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -664,6 +698,7 @@ select {
} }
body.loading #version, body.loading #version,
#loading .inner::after,
.modal.loading .content::after, .modal.loading .content::after,
.reasoning .toggle::before, .reasoning .toggle::before,
.reasoning .toggle::after, .reasoning .toggle::after,
@@ -992,6 +1027,20 @@ label[for="reasoning-tokens"] {
background: #89bb77; background: #89bb77;
} }
@keyframes wiggling {
0% {
transform: translate(0px);
}
50% {
transform: translate(-10px, 0px);
}
100% {
transform: translate(0px);
}
}
@keyframes rotating { @keyframes rotating {
from { from {
transform: rotate(0deg); transform: rotate(0deg);

View File

@@ -18,6 +18,12 @@
<body class="loading"> <body class="loading">
<div id="version"></div> <div id="version"></div>
<div id="loading">
<div class="inner">
<img src="logo.png" /> <span>whiskr</span>
</div>
</div>
<div id="page"> <div id="page">
<div id="messages"></div> <div id="messages"></div>
<div id="chat"> <div id="chat">

View File

@@ -51,6 +51,12 @@
}, 0); }, 0);
} }
function preloadIcons(icons) {
for (const icon of icons) {
new Image().src = `/css/icons/${icon}`;
}
}
function mark(index) { function mark(index) {
for (let x = 0; x < messages.length; x++) { for (let x = 0; x < messages.length; x++) {
messages[x].mark(Number.isInteger(index) && x >= index); messages[x].mark(Number.isInteger(index) && x >= index);
@@ -977,6 +983,9 @@
return false; return false;
} }
// start icon preload
preloadIcons(data.icons);
// render version // render version
if (data.version === "dev") { if (data.version === "dev") {
$version.remove(); $version.remove();
@@ -1416,5 +1425,9 @@
restore(); restore();
document.body.classList.remove("loading"); document.body.classList.remove("loading");
setTimeout(() => {
document.getElementById("loading").remove();
}, 500);
}); });
})(); })();