1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-09-08 17:06:42 +00:00

json & web search

This commit is contained in:
Laura
2025-08-11 00:15:58 +02:00
parent 5ae60e0f94
commit a8cbef7c7b
19 changed files with 374 additions and 59 deletions

View File

@@ -18,12 +18,13 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode
- Search field with fuzzy matching to quickly find models - Search field with fuzzy matching to quickly find models
- Models are listed newest -> oldest - Models are listed newest -> oldest
- Reasoning effort control - Reasoning effort control
- Web search tool
- Structured JSON output
## TODO ## TODO
- Retry button for assistant messages - Retry button for assistant messages
- Import and export of chats - Import and export of chats
- Web search tool
- Image and file attachments - Image and file attachments
## Built With ## Built With

14
chat.go
View File

@@ -25,6 +25,8 @@ type Request struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Model string `json:"model"` Model string `json:"model"`
Temperature float64 `json:"temperature"` Temperature float64 `json:"temperature"`
JSON bool `json:"json"`
Search bool `json:"search"`
Reasoning Reasoning `json:"reasoning"` Reasoning Reasoning `json:"reasoning"`
Messages []Message `json:"messages"` Messages []Message `json:"messages"`
} }
@@ -60,6 +62,18 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
} }
} }
if model.JSON && r.JSON {
request.ResponseFormat = &openrouter.ChatCompletionResponseFormat{
Type: openrouter.ChatCompletionResponseFormatTypeJSONObject,
}
}
if r.Search {
request.Plugins = append(request.Plugins, openrouter.ChatCompletionPlugin{
ID: openrouter.PluginIDWeb,
})
}
prompt, err := BuildPrompt(r.Prompt, model) prompt, err := BuildPrompt(r.Prompt, model)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -42,7 +42,7 @@ func cache(next http.Handler) http.Handler {
path := strings.ToLower(r.URL.Path) path := strings.ToLower(r.URL.Path)
ext := filepath.Ext(path) ext := filepath.Ext(path)
if ext == ".svg" || ext == ".ttf" || strings.HasSuffix(path, ".min.js") { if ext == ".svg" || ext == ".ttf" || strings.HasSuffix(path, ".min.js") || strings.HasSuffix(path, ".min.css") {
w.Header().Set("Cache-Control", "public, max-age=3024000, immutable") w.Header().Set("Cache-Control", "public, max-age=3024000, immutable")
} }

View File

@@ -15,6 +15,7 @@ type Model struct {
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Reasoning bool `json:"-"` Reasoning bool `json:"-"`
JSON bool `json:"-"`
} }
var ModelMap = make(map[string]*Model) var ModelMap = make(map[string]*Model)
@@ -40,7 +41,7 @@ func LoadModels() ([]*Model, error) {
name = name[index+2:] name = name[index+2:]
} }
tags, reasoning := GetModelTags(model) tags, reasoning, json := GetModelTags(model)
m := &Model{ m := &Model{
ID: model.ID, ID: model.ID,
@@ -49,6 +50,7 @@ func LoadModels() ([]*Model, error) {
Tags: tags, Tags: tags,
Reasoning: reasoning, Reasoning: reasoning,
JSON: json,
} }
models[index] = m models[index] = m
@@ -59,19 +61,25 @@ func LoadModels() ([]*Model, error) {
return models, nil return models, nil
} }
func GetModelTags(model openrouter.Model) ([]string, bool) { func GetModelTags(model openrouter.Model) ([]string, bool, bool) {
var ( var (
reasoning bool reasoning bool
json bool
tags []string tags []string
) )
for _, parameter := range model.SupportedParameters { for _, parameter := range model.SupportedParameters {
if parameter == "reasoning" { switch parameter {
case "reasoning":
reasoning = true reasoning = true
}
if parameter == "reasoning" || parameter == "tools" { tags = append(tags, "reasoning")
tags = append(tags, parameter) case "response_format":
json = true
tags = append(tags, "json")
case "tools":
tags = append(tags, "tools")
} }
} }
@@ -83,5 +91,5 @@ func GetModelTags(model openrouter.Model) ([]string, bool) {
sort.Strings(tags) sort.Strings(tags)
return tags, reasoning return tags, reasoning, json
} }

View File

@@ -137,21 +137,43 @@ body {
.message .role { .message .role {
position: absolute; position: absolute;
display: flex;
gap: 4px;
align-items: center;
font-family: "Comic Code", ui-monospace, "Cascadia Mono", "Segoe UI Mono", "Ubuntu Mono", "Roboto Mono", Menlo, Monaco, Consolas, monospace; font-family: "Comic Code", ui-monospace, "Cascadia Mono", "Segoe UI Mono", "Ubuntu Mono", "Roboto Mono", Menlo, Monaco, Consolas, monospace;
font-size: 12px; font-size: 12px;
line-height: 12px; line-height: 12px;
top: 8px; top: 6px;
left: 6px; left: 6px;
padding-left: 20px;
} }
#messages .message .role::before { .message .tags::before {
content: ""; content: "";
width: 16px;
height: 16px;
position: absolute; position: absolute;
top: -1px; top: 7px;
left: 0; left: -10px;
height: 2px;
width: 5px;
background: #939ab7;
}
.message .tags {
display: flex;
gap: 4px;
position: relative;
margin-left: 12px;
}
.message:not(.has-tags) .tags {
display: none;
}
#messages .message .tag-json {
background-image: url(icons/json-mode.svg);
}
#messages .message .tag-search {
background-image: url(icons/search-tool.svg);
} }
.message.user .role::before { .message.user .role::before {
@@ -187,15 +209,28 @@ body {
display: none; display: none;
} }
.message .reasoning, #messages .message .reasoning,
.message div.text { #messages .message div.text {
background: #24273a; background: #24273a;
} }
.message textarea.text { #messages .message textarea.text {
background: #181926; background: #181926;
} }
.message .text .error {
color: #ed8796;
}
.message.errored {
border: 2px solid #ed8796;
}
.message.errored .options .copy,
.message.errored .options .edit {
display: none;
}
.reasoning-text pre { .reasoning-text pre {
background: #1b1d2a; background: #1b1d2a;
} }
@@ -387,14 +422,18 @@ select {
.reasoning .toggle::before, .reasoning .toggle::before,
.reasoning .toggle::after, .reasoning .toggle::after,
#bottom, #bottom,
.message .role::before, #messages .message .role::before,
#messages .message .tag-json,
#messages .message .tag-search,
#json,
#search,
#scrolling, #scrolling,
#clear, #clear,
#add, #add,
#send, #send,
.pre-copy, .pre-copy,
.message .copy, #messages .message .copy,
.message .edit, #messages .message .edit,
.message .delete, .message .delete,
#chat .option label { #chat .option label {
display: block; display: block;
@@ -405,8 +444,20 @@ select {
background-repeat: no-repeat; background-repeat: no-repeat;
} }
#messages .message .tag-json,
#messages .message .tag-search,
#messages .message .role::before {
content: "";
width: 16px;
height: 16px;
}
input.invalid {
border: 1px solid #ed8796;
}
.pre-copy, .pre-copy,
.message .copy { #messages .message .copy {
background-image: url(icons/copy.svg); background-image: url(icons/copy.svg);
} }
@@ -415,11 +466,11 @@ select {
background-image: url(icons/check.svg); background-image: url(icons/check.svg);
} }
.message .edit { #messages .message .edit {
background-image: url(icons/edit.svg); background-image: url(icons/edit.svg);
} }
.message.editing .edit { #messages .message.editing .edit {
background-image: url(icons/save.svg); background-image: url(icons/save.svg);
} }
@@ -494,8 +545,14 @@ label[for="reasoning-tokens"] {
background-image: url(icons/add.svg); background-image: url(icons/add.svg);
} }
#scrolling { #json,
#search,
#scrolling,
#clear {
position: unset !important; position: unset !important;
}
#scrolling {
background-image: url(icons/screen-slash.svg); background-image: url(icons/screen-slash.svg);
} }
@@ -503,8 +560,23 @@ label[for="reasoning-tokens"] {
background-image: url(icons/screen.svg); background-image: url(icons/screen.svg);
} }
#json {
background-image: url(icons/json-off.svg);
}
#json.on {
background-image: url(icons/json-on.svg);
}
#search {
background-image: url(icons/search-off.svg);
}
#search.on {
background-image: url(icons/search-on.svg);
}
#clear { #clear {
position: unset !important;
background-image: url(icons/trash.svg); background-image: url(icons/trash.svg);
} }

View File

@@ -106,6 +106,14 @@
background-image: url(icons/tags/vision.svg) background-image: url(icons/tags/vision.svg)
} }
.tags .tag.json {
background-image: url(icons/tags/json.svg)
}
.tags .tag.all {
background-image: url(icons/tags/all.svg)
}
.dropdown .search { .dropdown .search {
background: #2a2e41; background: #2a2e41;
border-top: 2px solid #494d64; border-top: 2px solid #494d64;

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 716 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 716 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 716 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 928 B

View File

@@ -0,0 +1,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>

After

Width:  |  Height:  |  Size: 602 B

View File

@@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
<link href="css/catppuccin.css" rel="stylesheet" /> <link href="lib/catppuccin.min.css" rel="stylesheet" />
<link href="css/dropdown.css" rel="stylesheet" /> <link href="css/dropdown.css" rel="stylesheet" />
<link href="css/markdown.css" rel="stylesheet" /> <link href="css/markdown.css" rel="stylesheet" />
<link href="css/chat.css" rel="stylesheet" /> <link href="css/chat.css" rel="stylesheet" />
@@ -63,6 +63,10 @@
<label for="reasoning-tokens" title="Maximum amount of reasoning tokens"></label> <label for="reasoning-tokens" title="Maximum amount of reasoning tokens"></label>
<input id="reasoning-tokens" type="number" min="2" max="1" step="0.05" value="0.85" /> <input id="reasoning-tokens" type="number" min="2" max="1" step="0.05" value="0.85" />
</div> </div>
<div class="option group">
<button id="json" class="none" title="Turn on structured json output"></button>
<button id="search" title="Turn on web-search (openrouter built-in)"></button>
</div>
<div class="option"> <div class="option">
<button id="scrolling" title="Turn on auto-scrolling"></button> <button id="scrolling" title="Turn on auto-scrolling"></button>
</div> </div>

View File

@@ -9,6 +9,8 @@
$temperature = document.getElementById("temperature"), $temperature = document.getElementById("temperature"),
$reasoningEffort = document.getElementById("reasoning-effort"), $reasoningEffort = document.getElementById("reasoning-effort"),
$reasoningTokens = document.getElementById("reasoning-tokens"), $reasoningTokens = document.getElementById("reasoning-tokens"),
$json = document.getElementById("json"),
$search = document.getElementById("search"),
$add = document.getElementById("add"), $add = document.getElementById("add"),
$send = document.getElementById("send"), $send = document.getElementById("send"),
$scrolling = document.getElementById("scrolling"), $scrolling = document.getElementById("scrolling"),
@@ -18,6 +20,8 @@
models = {}; models = {};
let autoScrolling = false, let autoScrolling = false,
jsonMode = false,
searchTool = false,
interacted = false; interacted = false;
function scroll(force = false) { function scroll(force = false) {
@@ -27,7 +31,7 @@
setTimeout(() => { setTimeout(() => {
$messages.scroll({ $messages.scroll({
top: $messages.scrollHeight + 200, top: $messages.scrollHeight,
behavior: "smooth", behavior: "smooth",
}); });
}, 0); }, 0);
@@ -39,6 +43,9 @@
#reasoning; #reasoning;
#text; #text;
#tags = [];
#error = false;
#editing = false; #editing = false;
#expanded = false; #expanded = false;
#state = false; #state = false;
@@ -48,7 +55,7 @@
#patching = {}; #patching = {};
#_message; #_message;
#_role; #_tags;
#_reasoning; #_reasoning;
#_text; #_text;
#_edit; #_edit;
@@ -75,10 +82,22 @@
// main message div // main message div
this.#_message = make("div", "message", this.#role); this.#_message = make("div", "message", this.#role);
// message role // message role (wrapper)
this.#_role = make("div", "role", this.#role); const _wrapper = make("div", "role", this.#role);
this.#_message.appendChild(this.#_role); this.#_message.appendChild(_wrapper);
// message role
const _role = make("div");
_role.textContent = this.#role;
_wrapper.appendChild(_role);
// message tags
this.#_tags = make("div", "tags");
_wrapper.appendChild(this.#_tags);
// message reasoning (wrapper) // message reasoning (wrapper)
const _reasoning = make("div", "reasoning"); const _reasoning = make("div", "reasoning");
@@ -233,8 +252,17 @@
} }
#render(only = false, noScroll = false) { #render(only = false, noScroll = false) {
if (!only || only === "role") { if (!only || only === "tags") {
this.#_role.textContent = this.#role; console.log(this.#tags);
this.#_tags.innerHTML = this.#tags
.map((tag) => `<div class="tag-${tag}" title="${tag}"></div>`)
.join("");
this.#_message.classList.toggle("has-tags", this.#tags.length > 0);
}
if (this.#error) {
return;
} }
if (!only || only === "reasoning") { if (!only || only === "reasoning") {
@@ -251,7 +279,13 @@
} }
if (!only || only === "text") { if (!only || only === "text") {
this.#patch("text", this.#_text, this.#text, () => { let text = this.#text;
if (this.#tags.includes("json")) {
text = `\`\`\`json\n${text}\n\`\`\``;
}
this.#patch("text", this.#_text, text, () => {
noScroll || scroll(); noScroll || scroll();
}); });
@@ -262,23 +296,47 @@
#save() { #save() {
storeValue( storeValue(
"messages", "messages",
messages.map((message) => message.getData(true)), messages.map((message) => message.getData(true)).filter(Boolean),
); );
} }
getData(includeReasoning = false) { getData(full = false) {
const data = { const data = {
role: this.#role, role: this.#role,
text: this.#text, text: this.#text,
}; };
if (this.#reasoning && includeReasoning) { if (this.#reasoning && full) {
data.reasoning = this.#reasoning; data.reasoning = this.#reasoning;
} }
if (this.#error && full) {
data.error = this.#error;
}
if (this.#tags.length && full) {
data.tags = this.#tags;
}
return data; return data;
} }
addTag(tag) {
if (this.#tags.includes(tag)) {
return;
}
this.#tags.push(tag);
this.#render("tags");
if (tag === "json") {
this.#render("text");
}
this.#save();
}
setState(state) { setState(state) {
if (this.#state === state) { if (this.#state === state) {
return; return;
@@ -309,6 +367,20 @@
this.#save(); this.#save();
} }
showError(error) {
this.#error = error;
this.#_message.classList.add("errored");
const _err = make("div", "error");
_err.textContent = this.#error;
this.#_text.appendChild(_err);
this.#save();
}
stopEdit() { stopEdit() {
if (!this.#editing) { if (!this.#editing) {
return; return;
@@ -379,7 +451,9 @@
const response = await fetch(url, options); const response = await fetch(url, options);
if (!response.ok) { if (!response.ok) {
throw new Error(response.statusText); const err = await response.json();
throw new Error(err?.error || response.statusText);
} }
const reader = response.body.getReader(), const reader = response.body.getReader(),
@@ -461,7 +535,7 @@
models[model.id] = model; models[model.id] = model;
} }
dropdown($model); dropdown($model, 4);
return modelList; return modelList;
} }
@@ -474,13 +548,29 @@
$reasoningEffort.value = loadValue("reasoning-effort", "medium"); $reasoningEffort.value = loadValue("reasoning-effort", "medium");
$reasoningTokens.value = loadValue("reasoning-tokens", 1024); $reasoningTokens.value = loadValue("reasoning-tokens", 1024);
if (loadValue("json")) {
$json.click();
}
if (loadValue("search")) {
$search.click();
}
if (loadValue("scrolling")) { if (loadValue("scrolling")) {
$scrolling.click(); $scrolling.click();
} }
loadValue("messages", []).forEach( loadValue("messages", []).forEach((message) => {
(message) => new Message(message.role, message.reasoning, message.text), const obj = new Message(message.role, message.reasoning, message.text);
);
if (message.error) {
obj.showError(message.error);
}
if (message.tags) {
message.tags.forEach((tag) => obj.addTag(tag));
}
});
scroll(true); scroll(true);
@@ -537,6 +627,8 @@
$reasoningEffort.parentNode.classList.add("none"); $reasoningEffort.parentNode.classList.add("none");
$reasoningTokens.parentNode.classList.add("none"); $reasoningTokens.parentNode.classList.add("none");
} }
$json.classList.toggle("none", !data?.tags.includes("json"));
}); });
$prompt.addEventListener("change", () => { $prompt.addEventListener("change", () => {
@@ -544,7 +636,15 @@
}); });
$temperature.addEventListener("input", () => { $temperature.addEventListener("input", () => {
storeValue("temperature", $temperature.value); const value = $temperature.value,
temperature = parseFloat(value);
storeValue("temperature", value);
$temperature.classList.toggle(
"invalid",
Number.isNaN(temperature) || temperature < 0 || temperature > 2,
);
}); });
$reasoningEffort.addEventListener("change", () => { $reasoningEffort.addEventListener("change", () => {
@@ -555,8 +655,32 @@
$reasoningTokens.parentNode.classList.toggle("none", !!effort); $reasoningTokens.parentNode.classList.toggle("none", !!effort);
}); });
$reasoningTokens.addEventListener("change", () => { $reasoningTokens.addEventListener("input", () => {
storeValue("reasoning-tokens", $reasoningTokens.value); const value = $reasoningTokens.value,
tokens = parseInt(value);
storeValue("reasoning-tokens", value);
$reasoningTokens.classList.toggle(
"invalid",
Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024,
);
});
$json.addEventListener("click", () => {
jsonMode = !jsonMode;
storeValue("json", jsonMode);
$json.classList.toggle("on", jsonMode);
});
$search.addEventListener("click", () => {
searchTool = !searchTool;
storeValue("search", searchTool);
$search.classList.toggle("on", searchTool);
}); });
$message.addEventListener("input", () => { $message.addEventListener("input", () => {
@@ -589,6 +713,8 @@
if (autoScrolling) { if (autoScrolling) {
$scrolling.title = "Turn off auto-scrolling"; $scrolling.title = "Turn off auto-scrolling";
$scrolling.classList.add("on"); $scrolling.classList.add("on");
scroll();
} else { } else {
$scrolling.title = "Turn on auto-scrolling"; $scrolling.title = "Turn on auto-scrolling";
$scrolling.classList.remove("on"); $scrolling.classList.remove("on");
@@ -612,7 +738,7 @@
const temperature = parseFloat($temperature.value); const temperature = parseFloat($temperature.value);
if (Number.isNaN(temperature) || temperature < 0 || temperature > 1) { if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
return; return;
} }
@@ -640,6 +766,8 @@
effort: effort, effort: effort,
tokens: tokens || 0, tokens: tokens || 0,
}, },
json: jsonMode,
search: searchTool,
messages: messages.map((message) => message.getData()), messages: messages.map((message) => message.getData()),
}; };
@@ -647,6 +775,14 @@
message.setState("waiting"); message.setState("waiting");
if (jsonMode) {
message.addTag("json");
}
if (searchTool) {
message.addTag("search");
}
stream( stream(
"/-/chat", "/-/chat",
{ {
@@ -678,6 +814,10 @@
message.setState("receiving"); message.setState("receiving");
message.addText(chunk.text); message.addText(chunk.text);
break;
case "error":
message.showError(chunk.text);
break; break;
} }
}, },

View File

@@ -5,13 +5,15 @@
#_selected; #_selected;
#_search; #_search;
#maxTags = false;
#search = false; #search = false;
#selected = false; #selected = false;
#options = []; #options = [];
constructor(el) { constructor(el, maxTags = false) {
this.#_select = el; this.#_select = el;
this.#maxTags = maxTags;
this.#search = "searchable" in el.dataset; this.#search = "searchable" in el.dataset;
this.#_select.querySelectorAll("option").forEach((option) => { this.#_select.querySelectorAll("option").forEach((option) => {
@@ -101,15 +103,23 @@
_opt.appendChild(_label); _opt.appendChild(_label);
// option tags (optional) // option tags (optional)
if (option.tags?.length) { const tags = option.tags;
if (option.tags.length) {
const _tags = make("div", "tags"); const _tags = make("div", "tags");
for (const tag of option.tags) { _tags.title = `${this.#maxTags ? `${tags.length}/${this.#maxTags}: ` : ""}${tags.join(", ")}`;
const _tag = make("div", "tag", tag);
_tag.title = tag; if (this.#maxTags && tags.length >= this.#maxTags) {
const _all = make("div", "tag", "all");
_tags.appendChild(_tag); _tags.appendChild(_all);
} else {
for (const tag of tags) {
const _tag = make("div", "tag", tag);
_tags.appendChild(_tag);
}
} }
_opt.appendChild(_tags); _opt.appendChild(_tags);
@@ -220,5 +230,5 @@
}); });
}); });
window.dropdown = (el) => new Dropdown(el); window.dropdown = (el, maxTags = false) => new Dropdown(el, maxTags);
})(); })();

View File

@@ -1,5 +1,7 @@
function storeValue(key, value) { /** biome-ignore-all lint/correctness/noUnusedVariables: utility */
if (!value) {
function storeValue(key, value = false) {
if (value === null || value === undefined || value === false) {
localStorage.removeItem(key); localStorage.removeItem(key);
return; return;
@@ -11,14 +13,14 @@ function storeValue(key, value) {
function loadValue(key, fallback = false) { function loadValue(key, fallback = false) {
const raw = localStorage.getItem(key); const raw = localStorage.getItem(key);
if (!raw) { if (raw === null) {
return fallback; return fallback;
} }
try { try {
const value = JSON.parse(raw); const value = JSON.parse(raw);
if (!value) { if (value === null) {
throw new Error("no value"); throw new Error("no value");
} }