From dcf7f0910862f19a70d401a8d3b81c0858cef69e Mon Sep 17 00:00:00 2001 From: Laura Date: Sun, 10 Aug 2025 16:38:02 +0200 Subject: [PATCH] tags and caching --- main.go | 17 ++++++++++++- models.go | 32 ++++++++++++++++++------- static/css/chat.css | 6 ++--- static/css/dropdown.css | 36 ++++++++++++++++++++++++++++ static/css/icons/tags/reasoning.svg | 7 ++++++ static/css/icons/tags/tools.svg | 7 ++++++ static/js/chat.js | 3 +++ static/js/dropdown.js | 37 ++++++++++++++++++++++++++--- static/js/lib.js | 4 +++- 9 files changed, 133 insertions(+), 16 deletions(-) create mode 100644 static/css/icons/tags/reasoning.svg create mode 100644 static/css/icons/tags/tools.svg diff --git a/main.go b/main.go index 0a77802..c9d535e 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import ( "net/http" + "path/filepath" + "strings" "github.com/coalaura/logger" adapter "github.com/coalaura/logger/http" @@ -23,7 +25,7 @@ func main() { r.Use(adapter.Middleware(log)) fs := http.FileServer(http.Dir("./static")) - r.Handle("/*", http.StripPrefix("/", fs)) + r.Handle("/*", cache(http.StripPrefix("/", fs))) r.Get("/-/models", func(w http.ResponseWriter, r *http.Request) { RespondJson(w, http.StatusOK, models) @@ -34,3 +36,16 @@ func main() { log.Debug("Listening at http://localhost:3443/") http.ListenAndServe(":3443", r) } + +func cache(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.ToLower(r.URL.Path) + ext := filepath.Ext(path) + + if ext == ".svg" || ext == ".ttf" || strings.HasSuffix(path, ".min.js") { + w.Header().Set("Cache-Control", "public, max-age=3024000, immutable") + } + + next.ServeHTTP(w, r) + }) +} diff --git a/models.go b/models.go index 493dcbb..738454d 100644 --- a/models.go +++ b/models.go @@ -4,13 +4,15 @@ import ( "context" "sort" "strings" + + "github.com/revrost/go-openrouter" ) type Model struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - SupportedParameters []string `json:"supported_parameters,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` } var ModelMap = make(map[string]*Model) @@ -37,10 +39,10 @@ func LoadModels() ([]*Model, error) { } m := &Model{ - ID: model.ID, - Name: name, - Description: model.Description, - SupportedParameters: model.SupportedParameters, + ID: model.ID, + Name: name, + Description: model.Description, + Tags: GetModelTags(model), } models[index] = m @@ -50,3 +52,17 @@ func LoadModels() ([]*Model, error) { return models, nil } + +func GetModelTags(model openrouter.Model) []string { + var tags []string + + for _, parameter := range model.SupportedParameters { + if parameter == "reasoning" || parameter == "tools" { + tags = append(tags, parameter) + } + } + + sort.Strings(tags) + + return tags +} diff --git a/static/css/chat.css b/static/css/chat.css index cb221d8..1c34461 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -34,13 +34,13 @@ } ::-webkit-scrollbar-track { - background: #181926; + background: transparent; } ::-webkit-scrollbar-thumb { background-color: #cad3f5; + border: none; border-radius: 4px; - border: 1px solid #181926; } ::-webkit-scrollbar-thumb:hover { @@ -49,7 +49,7 @@ * { scrollbar-width: thin; - scrollbar-color: #cad3f5 #181926; + scrollbar-color: #cad3f5 transparent; } html, diff --git a/static/css/dropdown.css b/static/css/dropdown.css index 8db5fd4..bf65a25 100644 --- a/static/css/dropdown.css +++ b/static/css/dropdown.css @@ -48,6 +48,14 @@ overflow-y: auto; } +.dropdown .selected, +.dropdown .opt { + display: flex; + justify-content: space-between; + align-items: center; + gap: 5px; +} + .dropdown .opt { padding: 4px 6px; cursor: pointer; @@ -66,6 +74,34 @@ display: none; } +.dropdown .label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 300px; +} + +.dropdown .tags { + display: flex; + gap: 2px; +} + +.dropdown .tags .tag { + width: 18px; + height: 18px; + background-position: center; + background-size: contain; + background-repeat: no-repeat; +} + +.tags .tag.reasoning { + background-image: url(icons/tags/reasoning.svg) +} + +.tags .tag.tools { + background-image: url(icons/tags/tools.svg) +} + .dropdown .search { background: #2a2e41; border-top: 2px solid #494d64; diff --git a/static/css/icons/tags/reasoning.svg b/static/css/icons/tags/reasoning.svg new file mode 100644 index 0000000..fb5d3e6 --- /dev/null +++ b/static/css/icons/tags/reasoning.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/static/css/icons/tags/tools.svg b/static/css/icons/tags/tools.svg new file mode 100644 index 0000000..e8d00b5 --- /dev/null +++ b/static/css/icons/tags/tools.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/static/js/chat.js b/static/js/chat.js index 2c138d2..b793d3a 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -448,8 +448,11 @@ const el = document.createElement("option"); el.value = model.id; + el.title = model.description; el.textContent = model.name; + el.dataset.tags = (model.tags || []).join(","); + $model.appendChild(el); } diff --git a/static/js/dropdown.js b/static/js/dropdown.js index 08bb763..99c6d21 100644 --- a/static/js/dropdown.js +++ b/static/js/dropdown.js @@ -15,9 +15,15 @@ this.#search = "searchable" in el.dataset; this.#_select.querySelectorAll("option").forEach((option) => { + const tags = option.dataset.tags?.trim(); + this.#options.push({ value: option.value, label: option.textContent, + + title: option.title || false, + tags: tags ? tags.split(",") : [], + search: searchable(option.textContent), }); }); @@ -75,9 +81,10 @@ // options for (const option of this.#options) { + // option wrapper const _opt = make("div", "opt"); - _opt.textContent = option.label; + _opt.title = option.title || ""; _opt.addEventListener("click", () => { this.#_select.value = option.value; @@ -85,6 +92,30 @@ this.#_dropdown.classList.remove("open"); }); + // option label + const _label = make("div", "label"); + + _label.title = option.label; + _label.textContent = option.label; + + _opt.appendChild(_label); + + // option tags (optional) + if (option.tags?.length) { + const _tags = make("div", "tags"); + + for (const tag of option.tags) { + const _tag = make("div", "tag", tag); + + _tag.title = tag; + + _tags.appendChild(_tag); + } + + _opt.appendChild(_tags); + } + + // add to options _options.appendChild(_opt); option.el = _opt; @@ -128,14 +159,14 @@ #render() { if (this.#selected === false) { - this.#_selected.textContent = ""; + this.#_selected.innerHTML = ""; return; } const selection = this.#options[this.#selected]; - this.#_selected.textContent = selection.label; + this.#_selected.innerHTML = selection.el.innerHTML; } #filter() { diff --git a/static/js/lib.js b/static/js/lib.js index 2aaf6a4..2c1f222 100644 --- a/static/js/lib.js +++ b/static/js/lib.js @@ -45,7 +45,9 @@ function uid() { function make(tag, ...classes) { const el = document.createElement(tag); - el.classList.add(...classes); + if (classes.length) { + el.classList.add(...classes); + } return el; }