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]
|
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
2
go.mod
@@ -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
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.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=
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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, "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user