1
0
mirror of https://github.com/coalaura/whiskr.git synced 2025-12-02 20:22:52 +00:00
This commit is contained in:
Laura
2025-09-12 14:34:08 +02:00
parent 5998f61823
commit ea65f7fd60
7 changed files with 143 additions and 29 deletions

21
chat.go
View File

@@ -393,6 +393,7 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
} }
choice := chunk.Choices[0] choice := chunk.Choices[0]
delta := choice.Delta
if choice.FinishReason == openrouter.FinishReasonContentFilter { if choice.FinishReason == openrouter.FinishReasonContentFilter {
response.Send(ErrorChunk(errors.New("stopped due to content_filter"))) response.Send(ErrorChunk(errors.New("stopped due to content_filter")))
@@ -400,7 +401,7 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
return nil, "", nil return nil, "", nil
} }
calls := choice.Delta.ToolCalls calls := delta.ToolCalls
if len(calls) > 0 { if len(calls) > 0 {
call := calls[0] call := calls[0]
@@ -416,14 +417,20 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
break break
} }
content := choice.Delta.Content if delta.Content != "" {
buf.WriteString(delta.Content)
if content != "" { response.Send(TextChunk(delta.Content))
buf.WriteString(content) } else if delta.Reasoning != nil {
response.Send(ReasoningChunk(*delta.Reasoning))
} else if len(delta.Images) > 0 {
for _, image := range delta.Images {
if image.Type != openrouter.StreamImageTypeImageURL {
continue
}
response.Send(TextChunk(content)) response.Send(ImageChunk(image.ImageURL.URL))
} else if choice.Delta.Reasoning != nil { }
response.Send(ReasoningChunk(*choice.Delta.Reasoning))
} }
} }

2
go.mod
View File

@@ -6,7 +6,7 @@ require (
github.com/coalaura/plain v0.2.0 github.com/coalaura/plain v0.2.0
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/goccy/go-yaml v1.18.0 github.com/goccy/go-yaml v1.18.0
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861 github.com/revrost/go-openrouter v0.2.4
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.42.0
) )

2
go.sum
View File

@@ -28,6 +28,8 @@ github.com/revrost/go-openrouter v0.2.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxn
github.com/revrost/go-openrouter v0.2.3/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0= github.com/revrost/go-openrouter v0.2.3/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861 h1:4XU64nIgj6l9659KJx+FOaABvdhM3YrytCgD8XoKu90= github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861 h1:4XU64nIgj6l9659KJx+FOaABvdhM3YrytCgD8XoKu90=
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0= github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/revrost/go-openrouter v0.2.4 h1:ts9VMZGj8C6688xIgBU9/Tyw2WBl55WfdVP2zG+EV98=
github.com/revrost/go-openrouter v0.2.4/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=

View File

@@ -334,6 +334,9 @@ body:not(.loading) #loading {
margin-left: 12px; margin-left: 12px;
} }
.message:not(.has-tool) .tool,
.message:not(.has-reasoning) .reasoning,
.message:not(.has-images) .images,
.message:not(.has-text) .text, .message:not(.has-text) .text,
.message:not(.has-tags) .tags { .message:not(.has-tags) .tags {
display: none; display: none;
@@ -405,6 +408,18 @@ body:not(.loading) #loading {
border: 2px solid #ed8796; border: 2px solid #ed8796;
} }
.images .image {
display: block;
font-size: 0;
}
.images .image img {
max-width: 100%;
width: 420px;
border-radius: 6px;
border: 2px solid #363a4f;
}
.tool .result pre, .tool .result pre,
.reasoning-text pre { .reasoning-text pre {
background: #1b1d2a; background: #1b1d2a;
@@ -433,8 +448,7 @@ body:not(.loading) #loading {
.message.has-reasoning:not(.has-text):not(.errored) div.text, .message.has-reasoning:not(.has-text):not(.errored) div.text,
.message.has-tool:not(.has-text):not(.errored) div.text, .message.has-tool:not(.has-text):not(.errored) div.text,
.message.has-files:not(.has-text):not(.errored) div.text, .message.has-files:not(.has-text):not(.errored) div.text,
.message:not(.has-tool) .tool, .message.has-images:not(.has-text):not(.errored) div.text {
.message:not(.has-reasoning) .reasoning {
display: none; display: none;
} }

View File

@@ -137,6 +137,7 @@
#role; #role;
#reasoning; #reasoning;
#text; #text;
#images = [];
#files = []; #files = [];
#tool; #tool;
@@ -158,10 +159,11 @@
#_reasoning; #_reasoning;
#_text; #_text;
#_edit; #_edit;
#_images;
#_tool; #_tool;
#_statistics; #_statistics;
constructor(role, reasoning, text, files = [], collapsed = false) { constructor(role, reasoning, text, tool = false, files = [], images = [], tags = [], collapsed = false) {
this.#id = uid(); this.#id = uid();
this.#role = role; this.#role = role;
this.#reasoning = reasoning || ""; this.#reasoning = reasoning || "";
@@ -172,15 +174,25 @@
this.#build(collapsed); this.#build(collapsed);
this.#render(); this.#render();
if (tool) {
this.setTool(tool);
}
for (const file of files) { for (const file of files) {
this.addFile(file); this.addFile(file);
} }
for (const image of images) {
this.addImage(image);
}
for (const tag of tags) {
this.addTag(tag);
}
messages.push(this); messages.push(this);
if (this.#reasoning || this.#text) { this.#save();
this.#save();
}
} }
#build(collapsed) { #build(collapsed) {
@@ -273,6 +285,11 @@
this.updateEditHeight(); this.updateEditHeight();
}); });
// message images
this.#_images = make("div", "images");
_body.appendChild(this.#_images);
// message tool // message tool
this.#_tool = make("div", "tool"); this.#_tool = make("div", "tool");
@@ -514,6 +531,38 @@
this.#_message.classList.toggle("has-tags", this.#tags.length > 0); this.#_message.classList.toggle("has-tags", this.#tags.length > 0);
} }
if (!only || only === "images") {
for (let x = 0; x < this.#images.length; x++) {
if (this.#_images.querySelector(`.i-${x}`)) {
continue;
}
const image = this.#images[x],
blob = dataBlob(image),
url = URL.createObjectURL(blob);
const _link = make("a", "image", `i-${x}`);
_link.download = `image-${x + 1}`;
_link.target = "_blank";
_link.href = url;
this.#_images.appendChild(_link);
const _image = make("img");
_image.src = url;
_link.appendChild(_image);
}
this.#_message.classList.toggle("has-images", !!this.#images.length);
noScroll || scroll();
updateScrollButton();
}
if (!only || only === "tool") { if (!only || only === "tool") {
if (this.#tool) { if (this.#tool) {
const { name, args, result, cost, invalid } = this.#tool; const { name, args, result, cost, invalid } = this.#tool;
@@ -650,6 +699,10 @@
data.reasoning = this.#reasoning; data.reasoning = this.#reasoning;
} }
if (this.#images.length && full) {
data.images = this.#images;
}
if (this.#error && full) { if (this.#error && full) {
data.error = this.#error; data.error = this.#error;
} }
@@ -666,7 +719,7 @@
data.collapsed = true; data.collapsed = true;
} }
if (!data.files?.length && !data.reasoning && !data.text && !data.tool) { if (!data.images?.length && !data.files?.length && !data.reasoning && !data.text && !data.tool) {
return false; return false;
} }
@@ -790,6 +843,13 @@
this.#save(); this.#save();
} }
addImage(image) {
this.#images.push(image);
this.#render("images");
this.#save();
}
addReasoning(chunk) { addReasoning(chunk) {
this.#reasoning += chunk; this.#reasoning += chunk;
@@ -995,7 +1055,7 @@
$temperature.classList.remove("invalid"); $temperature.classList.remove("invalid");
} }
let iterations = parseInt($iterations.value); let iterations = parseInt($iterations.value, 10);
if (Number.isNaN(iterations) || iterations < 1 || iterations > 50) { if (Number.isNaN(iterations) || iterations < 1 || iterations > 50) {
iterations = 3; iterations = 3;
@@ -1006,7 +1066,7 @@
const effort = $reasoningEffort.value; const effort = $reasoningEffort.value;
let tokens = parseInt($reasoningTokens.value); let tokens = parseInt($reasoningTokens.value, 10);
if (!effort && (Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024)) { if (!effort && (Number.isNaN(tokens) || tokens <= 0 || tokens > 1024 * 1024)) {
tokens = 1024; tokens = 1024;
@@ -1146,6 +1206,10 @@
finish(); finish();
} }
break;
case "image":
message.addImage(chunk.text);
break; break;
case "reason": case "reason":
message.setState("reasoning"); message.setState("reasoning");
@@ -1354,22 +1418,12 @@
} }
loadValue("messages", []).forEach(message => { loadValue("messages", []).forEach(message => {
const obj = new Message(message.role, message.reasoning, message.text, message.files || [], message.collapsed); const obj = new Message(message.role, message.reasoning, message.text, message.tool, message.files || [], message.images || [], message.tags || [], message.collapsed);
if (message.error) { if (message.error) {
obj.showError(message.error); obj.showError(message.error);
} }
if (message.tags) {
message.tags.forEach(tag => {
obj.addTag(tag);
});
}
if (message.tool) {
obj.setTool(message.tool);
}
if (message.statistics) { if (message.statistics) {
obj.setStatistics(message.statistics); obj.setStatistics(message.statistics);
} }
@@ -1689,7 +1743,7 @@
}, },
json: jsonMode, json: jsonMode,
search: searchTool, search: searchTool,
messages: messages.map(message => message.getData()).filter(Boolean), messages: messages.map(message => message.getData(true)).filter(Boolean),
}); });
download("chat.json", "application/json", data); download("chat.json", "application/json", data);

View File

@@ -86,6 +86,36 @@ function formatMilliseconds(ms) {
return `${Math.round(ms / 1000)}s`; return `${Math.round(ms / 1000)}s`;
} }
function dataBlob(dataUrl) {
const [header, data] = dataUrl.split(","),
mime = header.match(/data:(.*?)(;|$)/)[1];
let blob;
if (header.includes(";base64")) {
const bytes = atob(data),
numbers = new Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
numbers[i] = bytes.charCodeAt(i);
}
const byteArray = new Uint8Array(numbers);
blob = new Blob([byteArray], {
type: mime,
});
} else {
const text = decodeURIComponent(data);
blob = new Blob([text], {
type: mime,
});
}
return blob;
}
function fixed(num, decimals = 0) { function fixed(num, decimals = 0) {
return num.toFixed(decimals).replace(/\.?0+$/m, ""); return num.toFixed(decimals).replace(/\.?0+$/m, "");
} }

View File

@@ -79,6 +79,13 @@ func TextChunk(text string) Chunk {
} }
} }
func ImageChunk(image string) Chunk {
return Chunk{
Type: "image",
Text: image,
}
}
func ToolChunk(tool *ToolCall) Chunk { func ToolChunk(tool *ToolCall) Chunk {
return Chunk{ return Chunk{
Type: "tool", Type: "tool",