From 1c4ff263782e8dd8c6d0bd37ce18b458b5c9c672 Mon Sep 17 00:00:00 2001 From: Laura Date: Thu, 11 Sep 2025 23:25:58 +0200 Subject: [PATCH] image generation wip --- chat.go | 8 ++++++++ env.go | 20 +++++++++++++++----- go.mod | 2 +- go.sum | 2 ++ models.go | 9 +++++++++ static/css/chat.css | 14 +++++++++++++- static/css/dropdown.css | 4 ++++ static/css/icons/tags/image.svg | 7 +++++++ static/js/chat.js | 31 ++++++++++++++++++++++++++++++- stream.go | 32 ++++++++++++++++---------------- 10 files changed, 105 insertions(+), 24 deletions(-) create mode 100644 static/css/icons/tags/image.svg diff --git a/chat.go b/chat.go index 9ce93f2..d102bc9 100644 --- a/chat.go +++ b/chat.go @@ -105,6 +105,14 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) { request.Model = r.Model + request.Modalities = []openrouter.ChatCompletionModality{ + openrouter.ModalityText, + } + + if env.Settings.ImageGeneration && model.Images { + request.Modalities = append(request.Modalities, openrouter.ModalityImage) + } + if r.Iterations < 1 || r.Iterations > 50 { return nil, fmt.Errorf("invalid iterations (1-50): %d", r.Iterations) } diff --git a/env.go b/env.go index 95a4b72..8e84c40 100644 --- a/env.go +++ b/env.go @@ -19,8 +19,9 @@ type EnvTokens struct { } type EnvSettings struct { - CleanContent bool `json:"cleanup"` - TitleModel string `json:"title-model"` + CleanContent bool `json:"cleanup"` + TitleModel string `json:"title-model"` + ImageGeneration bool `json:"image-generation"` } type EnvUser struct { @@ -45,7 +46,8 @@ type Environment struct { var env = Environment{ // defaults Settings: EnvSettings{ - CleanContent: true, + CleanContent: true, + ImageGeneration: true, }, } @@ -67,6 +69,13 @@ func (e *Environment) Init() error { log.Warnln("Debug mode enabled") } + // print if image generation is enabled + if e.Settings.ImageGeneration { + log.Warnln("Image generation enabled") + } else { + log.Warnln("Image generation disabled") + } + // check if server secret is set if e.Tokens.Secret == "" { log.Warnln("Missing tokens.secret, generating new...") @@ -133,8 +142,9 @@ func (e *Environment) Store() error { "$.tokens.exa": {yaml.HeadComment(" exa search api token (optional; used by search tools)")}, "$.tokens.github": {yaml.HeadComment(" github api token (optional; used by search tools)")}, - "$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")}, - "$.settings.title-model": {yaml.HeadComment(" model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)")}, + "$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")}, + "$.settings.title-model": {yaml.HeadComment(" model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)")}, + "$.settings.image-generation": {yaml.HeadComment(" allow image generation (optional; default: true)")}, "$.authentication.enabled": {yaml.HeadComment(" require login with username and password")}, "$.authentication.users": {yaml.HeadComment(" list of users with bcrypt password hashes")}, diff --git a/go.mod b/go.mod index ff59ad1..108450e 100644 --- a/go.mod +++ b/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.3 + github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861 golang.org/x/crypto v0.42.0 ) diff --git a/go.sum b/go.sum index 1a2c4eb..711ed5a 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/revrost/go-openrouter v0.2.2 h1:7bOdLPKmw0iJB1AdpN+YaWUd2XC9cwfJKDY10 github.com/revrost/go-openrouter v0.2.2/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0= github.com/revrost/go-openrouter v0.2.3 h1:ollIaPrgVWgqJyKbJGSX1jFs66eAWJs8Ojrxnd2i/E0= 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/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= diff --git a/models.go b/models.go index ce61639..1a6c016 100644 --- a/models.go +++ b/models.go @@ -18,6 +18,7 @@ type Model struct { Vision bool `json:"-"` JSON bool `json:"-"` Tools bool `json:"-"` + Images bool `json:"-"` } var ModelMap = make(map[string]*Model) @@ -89,5 +90,13 @@ func GetModelTags(model openrouter.Model, m *Model) { } } + for _, modality := range model.Architecture.OutputModalities { + if modality == "image" { + m.Images = true + + m.Tags = append(m.Tags, "image") + } + } + sort.Strings(m.Tags) } diff --git a/static/css/chat.css b/static/css/chat.css index e0f1da2..1df2602 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -363,6 +363,18 @@ body:not(.loading) #loading { max-width: 800px; } +.message.loading .text::after { + content: ""; + display: block; + position: relative; + width: 32px; + height: 3px; + background: #cad3f5; + animation: swivel 1.5s ease-in-out infinite; + opacity: 0.5; + margin-top: 5px; +} + .message:not(.editing) textarea.text, .message.editing div.text { display: none; @@ -1256,7 +1268,7 @@ label[for="reasoning-tokens"] { } 50% { - left: calc(100%); + left: 100%; transform: translateX(-100%); } } diff --git a/static/css/dropdown.css b/static/css/dropdown.css index cda40e5..9930f06 100644 --- a/static/css/dropdown.css +++ b/static/css/dropdown.css @@ -111,6 +111,10 @@ background-image: url(icons/tags/json.svg) } +.tags .tag.image { + background-image: url(icons/tags/image.svg) +} + .tags .tag.all { background-image: url(icons/tags/all.svg) } diff --git a/static/css/icons/tags/image.svg b/static/css/icons/tags/image.svg new file mode 100644 index 0000000..57ca652 --- /dev/null +++ b/static/css/icons/tags/image.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/static/js/chat.js b/static/js/chat.js index cf08abe..27ad799 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -146,6 +146,7 @@ #editing = false; #state = false; + #loading = false; #_diff; #pending = {}; @@ -750,6 +751,16 @@ this.#save(); } + setLoading(loading) { + if (this.#loading === loading) { + return; + } + + this.#loading = loading; + + this.#_message.classList.toggle("loading", this.#loading); + } + setState(state) { if (this.#state === state) { return; @@ -1032,7 +1043,21 @@ messages: messages.map(message => message.getData()).filter(Boolean), }; - let message, generationID; + let message, generationID, timeout; + + function stopLoadingTimeout() { + clearTimeout(timeout); + + message?.setLoading(false); + } + + function startLoadingTimeout() { + clearTimeout(timeout); + + timeout = setTimeout(() => { + message?.setLoading(true); + }, 1500); + } function finish(aborted = false) { if (!message) { @@ -1076,6 +1101,8 @@ signal: chatController.signal, }, chunk => { + stopLoadingTimeout(); + if (chunk === "aborted") { chatController = null; @@ -1135,6 +1162,8 @@ break; } + + startLoadingTimeout(); } ); } diff --git a/stream.go b/stream.go index c83f723..b669931 100644 --- a/stream.go +++ b/stream.go @@ -52,6 +52,19 @@ func (s *Stream) Send(ch Chunk) error { return WriteChunk(s.wr, s.ctx, ch) } +func StartChunk() Chunk { + return Chunk{ + Type: "start", + } +} + +func IDChunk(id string) Chunk { + return Chunk{ + Type: "id", + Text: id, + } +} + func ReasoningChunk(text string) Chunk { return Chunk{ Type: "reason", @@ -73,10 +86,10 @@ func ToolChunk(tool *ToolCall) Chunk { } } -func IDChunk(id string) Chunk { +func ErrorChunk(err error) Chunk { return Chunk{ - Type: "id", - Text: id, + Type: "error", + Text: GetErrorMessage(err), } } @@ -86,19 +99,6 @@ func EndChunk() Chunk { } } -func StartChunk() Chunk { - return Chunk{ - Type: "start", - } -} - -func ErrorChunk(err error) Chunk { - return Chunk{ - Type: "error", - Text: GetErrorMessage(err), - } -} - func GetErrorMessage(err error) string { if apiErr, ok := err.(*openrouter.APIError); ok { return apiErr.Error()