mirror of
https://github.com/coalaura/whiskr.git
synced 2025-12-02 20:22:52 +00:00
images
This commit is contained in:
21
chat.go
21
chat.go
@@ -393,6 +393,7 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
|
||||
}
|
||||
|
||||
choice := chunk.Choices[0]
|
||||
delta := choice.Delta
|
||||
|
||||
if choice.FinishReason == openrouter.FinishReasonContentFilter {
|
||||
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
|
||||
}
|
||||
|
||||
calls := choice.Delta.ToolCalls
|
||||
calls := delta.ToolCalls
|
||||
|
||||
if len(calls) > 0 {
|
||||
call := calls[0]
|
||||
@@ -416,14 +417,20 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
|
||||
break
|
||||
}
|
||||
|
||||
content := choice.Delta.Content
|
||||
if delta.Content != "" {
|
||||
buf.WriteString(delta.Content)
|
||||
|
||||
if content != "" {
|
||||
buf.WriteString(content)
|
||||
response.Send(TextChunk(delta.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))
|
||||
} else if choice.Delta.Reasoning != nil {
|
||||
response.Send(ReasoningChunk(*choice.Delta.Reasoning))
|
||||
response.Send(ImageChunk(image.ImageURL.URL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/coalaura/plain v0.2.0
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
go.sum
@@ -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.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 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/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
|
||||
@@ -334,6 +334,9 @@ body:not(.loading) #loading {
|
||||
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-tags) .tags {
|
||||
display: none;
|
||||
@@ -405,6 +408,18 @@ body:not(.loading) #loading {
|
||||
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,
|
||||
.reasoning-text pre {
|
||||
background: #1b1d2a;
|
||||
@@ -433,8 +448,7 @@ body:not(.loading) #loading {
|
||||
.message.has-reasoning: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:not(.has-tool) .tool,
|
||||
.message:not(.has-reasoning) .reasoning {
|
||||
.message.has-images:not(.has-text):not(.errored) div.text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
#role;
|
||||
#reasoning;
|
||||
#text;
|
||||
#images = [];
|
||||
#files = [];
|
||||
|
||||
#tool;
|
||||
@@ -158,10 +159,11 @@
|
||||
#_reasoning;
|
||||
#_text;
|
||||
#_edit;
|
||||
#_images;
|
||||
#_tool;
|
||||
#_statistics;
|
||||
|
||||
constructor(role, reasoning, text, files = [], collapsed = false) {
|
||||
constructor(role, reasoning, text, tool = false, files = [], images = [], tags = [], collapsed = false) {
|
||||
this.#id = uid();
|
||||
this.#role = role;
|
||||
this.#reasoning = reasoning || "";
|
||||
@@ -172,16 +174,26 @@
|
||||
this.#build(collapsed);
|
||||
this.#render();
|
||||
|
||||
if (tool) {
|
||||
this.setTool(tool);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
this.addFile(file);
|
||||
}
|
||||
|
||||
for (const image of images) {
|
||||
this.addImage(image);
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
this.addTag(tag);
|
||||
}
|
||||
|
||||
messages.push(this);
|
||||
|
||||
if (this.#reasoning || this.#text) {
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
#build(collapsed) {
|
||||
// main message div
|
||||
@@ -273,6 +285,11 @@
|
||||
this.updateEditHeight();
|
||||
});
|
||||
|
||||
// message images
|
||||
this.#_images = make("div", "images");
|
||||
|
||||
_body.appendChild(this.#_images);
|
||||
|
||||
// message tool
|
||||
this.#_tool = make("div", "tool");
|
||||
|
||||
@@ -514,6 +531,38 @@
|
||||
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 (this.#tool) {
|
||||
const { name, args, result, cost, invalid } = this.#tool;
|
||||
@@ -650,6 +699,10 @@
|
||||
data.reasoning = this.#reasoning;
|
||||
}
|
||||
|
||||
if (this.#images.length && full) {
|
||||
data.images = this.#images;
|
||||
}
|
||||
|
||||
if (this.#error && full) {
|
||||
data.error = this.#error;
|
||||
}
|
||||
@@ -666,7 +719,7 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -790,6 +843,13 @@
|
||||
this.#save();
|
||||
}
|
||||
|
||||
addImage(image) {
|
||||
this.#images.push(image);
|
||||
|
||||
this.#render("images");
|
||||
this.#save();
|
||||
}
|
||||
|
||||
addReasoning(chunk) {
|
||||
this.#reasoning += chunk;
|
||||
|
||||
@@ -995,7 +1055,7 @@
|
||||
$temperature.classList.remove("invalid");
|
||||
}
|
||||
|
||||
let iterations = parseInt($iterations.value);
|
||||
let iterations = parseInt($iterations.value, 10);
|
||||
|
||||
if (Number.isNaN(iterations) || iterations < 1 || iterations > 50) {
|
||||
iterations = 3;
|
||||
@@ -1006,7 +1066,7 @@
|
||||
|
||||
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)) {
|
||||
tokens = 1024;
|
||||
@@ -1146,6 +1206,10 @@
|
||||
finish();
|
||||
}
|
||||
|
||||
break;
|
||||
case "image":
|
||||
message.addImage(chunk.text);
|
||||
|
||||
break;
|
||||
case "reason":
|
||||
message.setState("reasoning");
|
||||
@@ -1354,22 +1418,12 @@
|
||||
}
|
||||
|
||||
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) {
|
||||
obj.showError(message.error);
|
||||
}
|
||||
|
||||
if (message.tags) {
|
||||
message.tags.forEach(tag => {
|
||||
obj.addTag(tag);
|
||||
});
|
||||
}
|
||||
|
||||
if (message.tool) {
|
||||
obj.setTool(message.tool);
|
||||
}
|
||||
|
||||
if (message.statistics) {
|
||||
obj.setStatistics(message.statistics);
|
||||
}
|
||||
@@ -1689,7 +1743,7 @@
|
||||
},
|
||||
json: jsonMode,
|
||||
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);
|
||||
|
||||
@@ -86,6 +86,36 @@ function formatMilliseconds(ms) {
|
||||
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) {
|
||||
return num.toFixed(decimals).replace(/\.?0+$/m, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user