mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-09 17:29:54 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ebb27ef34e | ||
![]() |
c24b0e87f7 | ||
fc27441bda | |||
89df106aa6 | |||
6bd6554997 | |||
![]() |
9f7f49b9eb | ||
![]() |
bde748ff0a |
31
README.md
31
README.md
@@ -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
10
env.go
@@ -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")},
|
||||||
|
@@ -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
|
||||||
|
36
main.go
36
main.go
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -11,13 +12,16 @@ import (
|
|||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
var log = logger.New().DetectTerminal().WithOptions(logger.Options{
|
var log = logger.New().DetectTerminal().WithOptions(logger.Options{
|
||||||
NoLevel: true,
|
NoLevel: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -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">
|
||||||
@@ -46,14 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="option">
|
<div class="option">
|
||||||
<label for="prompt" title="Main system prompt"></label>
|
<label for="prompt" title="Main system prompt"></label>
|
||||||
<select id="prompt">
|
<select id="prompt" data-searchable></select>
|
||||||
<option value="" selected>No Prompt</option>
|
|
||||||
<option value="normal">Assistant</option>
|
|
||||||
<option value="reviewer">Code Reviewer</option>
|
|
||||||
<option value="engineer">Prompt Engineer</option>
|
|
||||||
<option value="scripts">Shell Scripter</option>
|
|
||||||
<option value="physics">Physics Explainer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="option">
|
<div class="option">
|
||||||
<label for="temperature" title="Temperature (0 - 2)"></label>
|
<label for="temperature" title="Temperature (0 - 2)"></label>
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
Reference in New Issue
Block a user