mirror of
https://github.com/coalaura/whiskr.git
synced 2025-12-02 20:22:52 +00:00
image generation wip
This commit is contained in:
8
chat.go
8
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)
|
||||
}
|
||||
|
||||
20
env.go
20
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")},
|
||||
|
||||
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.3
|
||||
github.com/revrost/go-openrouter v0.2.4-0.20250909110314-b8c4ee4c5861
|
||||
golang.org/x/crypto v0.42.0
|
||||
)
|
||||
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
7
static/css/icons/tags/image.svg
Normal file
7
static/css/icons/tags/image.svg
Normal 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: 1.1 KiB |
@@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
32
stream.go
32
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()
|
||||
|
||||
Reference in New Issue
Block a user