SSE to WebSocket

I needed to ping pong the content of the message because it was too long
for a SSE event. I swaped to websocket instead
This commit is contained in:
Adrien Bouvais 2024-08-05 18:22:51 +02:00
parent c7924476ee
commit 3266c8787e
13 changed files with 720 additions and 203 deletions

123
Chat.go
View File

@ -59,6 +59,7 @@ type TemplateMessage struct {
Hidden bool
Id string
Name string
LLMID string
Model string
ModelID string
}
@ -111,7 +112,7 @@ func generateChatHTML(c *fiber.Ctx) string {
panic(err)
}
htmlString := "<div class='columns is-centered' id='chat-container' sse-connect='/sse?userID=" + user.ID.String() + "'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
htmlString := "<div class='columns is-centered' id='chat-container' ws-connect='/ws?userID=" + user.ID.String() + "'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
var templateMessages []TemplateMessage
@ -174,7 +175,7 @@ func GetUserMessageHandler(c *fiber.Ctx) error {
messageUUID, _ := edgedb.ParseUUID(id)
var selectedMessage Message
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(context.Background(), `
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
SELECT Message {
content
}
@ -199,12 +200,24 @@ func GetMessageContentHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
onlyContent := c.FormValue("onlyContent") // To init the text area of the edit message form
out := GenerateMessageContentHTML(c.Cookies("jade-edgedb-auth-token"), messageId, onlyContent, false)
return c.SendString(out)
}
func GenerateMessageContentHTML(authCookie string, messageId string, onlyContent string, withDiv bool) string {
fmt.Println("Generating message for:", authCookie)
messageUUID, _ := edgedb.ParseUUID(messageId)
var selectedMessage Message
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(context.Background(), `
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}).QuerySingle(edgeCtx, `
SELECT Message {
content,
area: {
id,
position,
},
llm : {
name,
modelInfo : {
@ -221,10 +234,18 @@ func GetMessageContentHandler(c *fiber.Ctx) error {
}
if onlyContent == "true" {
return c.SendString(markdownToHTML(selectedMessage.Content))
return markdownToHTML(selectedMessage.Content)
}
out := "<div class='message-header'>"
var out string
if withDiv {
out = fmt.Sprintf("<div class='message-content' id='content-%d' style='width: 100%%; overflow-y: hidden'>", selectedMessage.Area.Position)
} else {
out = ""
}
out += "<div class='message-header'>"
out += "<p>"
out += "<strong>" + selectedMessage.LLM.Name + "</strong> <small>" + selectedMessage.LLM.Model.ModelID + "</small>"
out += " </p>"
@ -235,8 +256,12 @@ func GetMessageContentHandler(c *fiber.Ctx) error {
out += " </ct>"
out += "</div>"
if withDiv {
out += "</div>"
}
// Update the selected value of messages in the database
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}).Execute(edgeCtx, `
WITH m := (SELECT Message FILTER .id = <uuid>$0)
UPDATE Message
FILTER .area = m.area
@ -246,13 +271,13 @@ func GetMessageContentHandler(c *fiber.Ctx) error {
panic(err)
}
_ = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
_ = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}).Execute(edgeCtx, `
UPDATE Message
FILTER .id = <uuid>$0
SET {selected := true};
`, messageUUID)
return c.SendString(out)
return out
}
func generateWelcomeChatHTML() string {
@ -550,62 +575,6 @@ func generateLimitReachedChatHTML(c *fiber.Ctx) string {
return htmlString
}
func GetSelectionBtnHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
messageUUID, err := edgedb.ParseUUID(messageId)
if err != nil {
fmt.Println("Error parsing UUID")
panic(err)
}
var message Message
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
SELECT Message {
id,
content,
area : {
id,
position
},
llm : {
modelInfo : {
modelID,
name,
company : {
icon,
}
}
}
}
FILTER .id = <uuid>$0;
`, &message, messageUUID)
if err != nil {
fmt.Println("Error getting message")
panic(err)
}
templateMessage := TemplateMessage{
Icon: message.LLM.Model.Company.Icon,
Content: message.Content,
Hidden: false,
Id: message.ID.String(),
Name: message.LLM.Model.Name,
ModelID: message.LLM.Model.ModelID,
}
outBtn, err := selectBtnTmpl.Execute(map[string]interface{}{
"message": templateMessage,
"ConversationAreaId": message.Area.Position,
})
if err != nil {
fmt.Println("Error generating HTML content")
panic(err)
}
outBtn = strings.ReplaceAll(outBtn, "\n", "")
return c.SendString(outBtn)
}
// Button actions
func DeleteMessageHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
@ -826,8 +795,6 @@ func LoadUsageKPIHandler(c *fiber.Ctx) error {
}
func GenerateModelPopoverHTML(refresh bool, c *fiber.Ctx) string {
openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists := getExistingKeys(c)
var llms []LLM
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Query(edgeCtx, `
SELECT LLM {
@ -873,20 +840,11 @@ func GenerateModelPopoverHTML(refresh bool, c *fiber.Ctx) string {
isPremium, _ := IsCurrentUserSubscribed(c)
out, err := modelPopoverTmpl.Execute(pongo2.Context{
"IsLogin": checkIfLogin(c),
"OpenaiExists": openaiExists,
"AnthropicExists": anthropicExists,
"MistralExists": mistralExists,
"GroqExists": groqExists,
"GooseaiExists": gooseaiExists,
"GoogleExists": googleExists,
"PerplexityExists": perplexityExists,
"FireworksExists": fireworksExists,
"AnyExists": fireworksExists || openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists || perplexityExists,
"LLMs": llms,
"ModelInfos": modelInfos,
"DeleteUpdate": refresh,
"IsPremium": isPremium,
"IsLogin": checkIfLogin(c),
"LLMs": llms,
"ModelInfos": modelInfos,
"DeleteUpdate": refresh,
"IsPremium": isPremium,
})
if err != nil {
fmt.Println("Error generating model popover")
@ -973,7 +931,7 @@ func LoadSettingsHandler(c *fiber.Ctx) error {
stripeSubLink := "https://billing.stripe.com/p/login/6oE6sc0PTfvq1Hi288?prefilled_email=" + user.Email
openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists := getExistingKeys(c)
openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists, nimExists := getExistingKeys(c)
isPremium, isBasic := IsCurrentUserSubscribed(c)
out, err := settingPopoverTmpl.Execute(pongo2.Context{
@ -984,9 +942,10 @@ func LoadSettingsHandler(c *fiber.Ctx) error {
"GroqExists": groqExists,
"GooseaiExists": gooseaiExists,
"GoogleExists": googleExists,
"NimExists": nimExists,
"PerplexityExists": perplexityExists,
"FireworksExists": fireworksExists,
"AnyExists": fireworksExists || openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists || perplexityExists,
"AnyExists": fireworksExists || openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists || perplexityExists || nimExists,
"isPremium": isPremium,
"isBasic": isBasic,
"StripeSubLink": stripeSubLink,

View File

@ -75,9 +75,9 @@ func addCodeHeader(htmlContent string, languages []string) string {
return updatedHTML
}
func getExistingKeys(c *fiber.Ctx) (bool, bool, bool, bool, bool, bool, bool, bool) {
func getExistingKeys(c *fiber.Ctx) (bool, bool, bool, bool, bool, bool, bool, bool, bool) {
if edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}) == nil {
return false, false, false, false, false, false, false, false
return false, false, false, false, false, false, false, false, false
}
var (
@ -86,6 +86,7 @@ func getExistingKeys(c *fiber.Ctx) (bool, bool, bool, bool, bool, bool, bool, bo
mistralExists bool
groqExists bool
gooseaiExists bool
nimExists bool
googleExists bool
perplexityExists bool
fireworksExists bool
@ -179,7 +180,18 @@ func getExistingKeys(c *fiber.Ctx) (bool, bool, bool, bool, bool, bool, bool, bo
panic(err)
}
return openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
select exists (
select global currentUser.setting.keys
filter .company.name = "nim"
);
`, &nimExists)
if err != nil {
fmt.Println("Error checking if Fireworks key exists")
panic(err)
}
return openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists, nimExists
}
func Message2RequestMessage(messages []Message, context string) []RequestMessage {

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
@ -96,8 +97,6 @@ func GeneratePlaceholderHTML(c *fiber.Ctx, message string, selectedLLMIds []stri
})
out += messageOut
// defer sendEvent("hide-placeholder", "")
return out
}
@ -116,6 +115,8 @@ func GenerateMultipleMessagesHandler(c *fiber.Ctx) error {
panic(err)
}
authCookie := c.Cookies("jade-edgedb-auth-token")
// Create a wait group to synchronize the goroutines
var wg sync.WaitGroup
@ -190,53 +191,40 @@ func GenerateMultipleMessagesHandler(c *fiber.Ctx) error {
panic(err)
}
// Check if the context's deadline is exceeded
select {
case <-ctx.Done():
// The context's deadline was exceeded
fmt.Printf("Goroutine %d timed out\n", idx)
default:
// Send the index of the completed goroutine to the firstDone channel
select {
case firstDone <- idx:
//Add the message as selected
_ = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
UPDATE Message
FILTER .id = <uuid>$0
SET {selected := true};
`, messageID)
// Generate the HTML content
outIcon := `<img src="` + selectedLLMs[idx].Model.Company.Icon + `" alt="User Image" id="selectedIcon-` + fmt.Sprintf("%d", message.Area.Position) + `">`
go func() {
// I do a ping because of sse size limit. Do see if it's possible to do without it TODO
sendEvent(
user.ID.String(),
"swapContent-"+fmt.Sprintf("%d", message.Area.Position),
`<hx hx-get="/messageContent?id=`+message.ID.String()+`" hx-trigger="load" hx-swap="outerHTML"></hx>`,
)
sendEvent(
user.ID.String(),
"swapSelectionBtn-"+selectedLLMs[idx].ID.String(),
`<hx hx-get="/selectionBtn?id=`+message.ID.String()+`" hx-trigger="load" hx-swap="outerHTML"></hx>`,
)
sendEvent(
user.ID.String(),
"swapIcon-"+fmt.Sprintf("%d", message.Area.Position),
outIcon,
)
}()
sendEvent(
user.ID.String(),
"swapContent-"+fmt.Sprintf("%d", message.Area.Position),
GenerateMessageContentHTML(
authCookie,
message.ID.String(),
"false",
true,
),
)
sendEvent(
user.ID.String(),
"swapSelectionBtn-"+selectedLLMs[idx].ID.String(),
GenerateSelectionBtnHTML(authCookie, message.ID.String()),
)
sendEvent(
user.ID.String(),
"swapIcon-"+fmt.Sprintf("%d", message.Area.Position),
outIcon,
)
default:
// Send Content event
go func() {
sendEvent(
user.ID.String(),
"swapSelectionBtn-"+selectedLLMs[idx].ID.String(),
`<hx hx-get="/selectionBtn?id=`+message.ID.String()+`" hx-trigger="load" hx-swap="outerHTML"></hx>`,
)
}()
sendEvent(
user.ID.String(),
"swapSelectionBtn-"+selectedLLMs[idx].ID.String(),
GenerateSelectionBtnHTML(authCookie, message.ID.String()),
)
}
}
}()
@ -250,6 +238,63 @@ func GenerateMultipleMessagesHandler(c *fiber.Ctx) error {
return c.SendString("")
}
func GenerateSelectionBtnHTML(authCookie string, messageID string) string {
messageUUID, err := edgedb.ParseUUID(messageID)
if err != nil {
fmt.Println("Error parsing UUID")
panic(err)
}
var message Message
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}).QuerySingle(edgeCtx, `
SELECT Message {
id,
content,
area : {
id,
position
},
llm : {
id,
modelInfo : {
modelID,
name,
company : {
icon,
}
}
}
}
FILTER .id = <uuid>$0;
`, &message, messageUUID)
if err != nil {
fmt.Println("Error getting message")
panic(err)
}
templateMessage := TemplateMessage{
Icon: message.LLM.Model.Company.Icon,
Content: message.Content,
Hidden: false,
Id: message.ID.String(),
LLMID: message.LLM.ID.String(),
Name: message.LLM.Model.Name,
ModelID: message.LLM.Model.ModelID,
}
outBtn, err := selectBtnTmpl.Execute(map[string]interface{}{
"message": templateMessage,
"ConversationAreaId": message.Area.Position,
})
if err != nil {
fmt.Println("Error generating HTML content")
panic(err)
}
outBtn = strings.ReplaceAll(outBtn, "\n", "")
return outBtn
}
func addUsage(c *fiber.Ctx, inputCost float32, outputCost float32, inputToken int32, outputToken int32, modelID string) {
// Create a new usage
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `

32
go.mod
View File

@ -3,9 +3,9 @@ module github.com/MrBounty/JADE2.0
go 1.22.2
require (
github.com/edgedb/edgedb-go v0.17.1
github.com/edgedb/edgedb-go v0.17.2
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3
github.com/gofiber/fiber/v2 v2.52.4
github.com/gofiber/fiber/v2 v2.52.5
github.com/gofiber/template/django/v3 v3.1.11
github.com/stripe/stripe-go v70.15.0+incompatible
github.com/yuin/goldmark v1.7.1
@ -14,29 +14,33 @@ require (
require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fasthttp/websocket v1.5.10 // indirect
github.com/flosch/pongo2/v6 v6.0.0 // indirect
github.com/gofiber/contrib/websocket v1.3.2 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xdg/scram v1.0.5 // indirect
github.com/xdg/stringprep v1.0.3 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.9.3 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
)

36
go.sum
View File

@ -2,6 +2,8 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -11,12 +13,20 @@ github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/edgedb/edgedb-go v0.17.1 h1:nWVNWq61X1KyJziy5Zm+NfUwr7nXiCW7/qmH1zMSOpI=
github.com/edgedb/edgedb-go v0.17.1/go.mod h1:J+llluepGAi/rIPNcUgIFEedCCISLKFG+VUEWnBhIqE=
github.com/edgedb/edgedb-go v0.17.2 h1:qp+HgwmLrT8d3agg4zZrjTJyVmoAuRvRPuGR6rwZ0ho=
github.com/edgedb/edgedb-go v0.17.2/go.mod h1:J+llluepGAi/rIPNcUgIFEedCCISLKFG+VUEWnBhIqE=
github.com/fasthttp/websocket v1.5.10 h1:bc7NIGyrg1L6sd5pRzCIbXpro54SZLEluZCu0rOpcN4=
github.com/fasthttp/websocket v1.5.10/go.mod h1:BwHeuXGWzCW1/BIKUKD3+qfCl+cTdsHu/f243NcAI/Q=
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 h1:fmFk0Wt3bBxxwZnu48jqMdaOR/IZ4vdtJFuaFV8MpIE=
github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3/go.mod h1:bJWSKrZyQvfTnb2OudyUjurSG4/edverV7n82+K3JiM=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRCJ0NWsV3cvg=
github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/django/v3 v3.1.11 h1:wE5k/wWNKGKxfeopaeB6IBijMiEVAxKHJVf1WMH5iNw=
@ -25,10 +35,14 @@ github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -42,11 +56,17 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -59,6 +79,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw=
@ -72,22 +94,36 @@ github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 h1:yHfZ
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594/go.mod h1:U9ihbh+1ZN7fR5Se3daSPoz1CGF9IYtSvWwVQtnzGHU=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

91
main.go
View File

@ -1,13 +1,13 @@
package main
import (
"bufio"
"fmt"
"log"
"os"
"sync"
"github.com/flosch/pongo2"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
@ -29,26 +29,55 @@ var (
explainLLMconvChatTmpl *pongo2.Template
mu sync.Mutex
app *fiber.App
userSSEChannels = make(map[string]chan SSE)
userWSChannels = make(map[string]*websocket.Conn)
)
// SSE event structure
type SSE struct {
Event string
Data string
}
// Function to send events to all clients
func sendEvent(userID string, event string, data string) {
message := fmt.Sprintf(`{"event": "%s", "data": "%s"}`, event, data)
sendWSMessage(userID, message)
}
func sendWSMessage(userID string, message string) {
mu.Lock()
defer mu.Unlock()
userEvents, ok := userSSEChannels[userID]
conn, ok := userWSChannels[userID]
if !ok {
return
}
userEvents <- SSE{Event: event, Data: data}
if err := conn.WriteMessage(websocket.TextMessage, []byte(message)); err != nil {
fmt.Println("write:", err)
conn.Close()
delete(userWSChannels, userID)
}
}
func handleWebSocket(c *websocket.Conn) {
userID := c.Query("userID")
if userID == "" {
c.Close()
return
}
mu.Lock()
userWSChannels[userID] = c
mu.Unlock()
defer func() {
mu.Lock()
delete(userWSChannels, userID)
mu.Unlock()
c.Close()
}()
for {
if _, _, err := c.ReadMessage(); err != nil {
log.Println("read:", err)
break
}
}
}
func main() {
@ -99,7 +128,6 @@ func main() {
app.Get("/userMessage", GetUserMessageHandler)
app.Post("/editMessage", EditMessageHandler)
app.Get("/help", generateHelpChatHandler)
app.Get("/selectionBtn", GetSelectionBtnHandler)
// Settings routes
app.Post("/addKeys", addKeys)
@ -136,7 +164,7 @@ func main() {
return c.SendString("")
})
app.Get("/sse", handleSSE)
app.Get("/ws", websocket.New(handleWebSocket))
// Start server
if err := app.Listen(":8080"); err != nil {
@ -144,45 +172,6 @@ func main() {
}
}
func handleSSE(c *fiber.Ctx) error {
userID := c.Query("userID") // Get userID from query parameter
if userID == "" {
return c.Status(fiber.StatusBadRequest).SendString("Missing userID")
}
events := make(chan SSE, 500)
mu.Lock()
userSSEChannels[userID] = events
mu.Unlock()
// Create a context copy to use in the goroutine
ctx := c.Context()
go func() {
<-ctx.Done()
mu.Lock()
delete(userSSEChannels, userID)
mu.Unlock()
close(events)
}()
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
for event := range events {
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Event, event.Data); err != nil {
fmt.Println(err)
return
}
w.Flush()
}
})
return nil
}
func addKeys(c *fiber.Ctx) error {
keys := map[string]string{
"openai": c.FormValue("openai_key"),

476
static/dependencies/ws.js Normal file
View File

@ -0,0 +1,476 @@
/*
WebSockets Extension
============================
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("ws", {
/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function (apiRef) {
// Store reference to internal API
api = apiRef;
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket;
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = "full-jitter";
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
var parent = evt.target || evt.detail.elt;
switch (name) {
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent)
if (internalData.webSocket) {
internalData.webSocket.close();
}
return;
// Try to create websockets when elements are processed
case "htmx:beforeProcessNode":
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
ensureWebSocketSend(child)
});
}
}
});
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} socketElt
* @returns
*/
function ensureWebSocket(socketElt) {
// If the element containing the WebSocket connection no longer exists, then
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) {
return;
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
if (wssSource == null || wssSource === "") {
var legacySource = getLegacyWebsocketURL(socketElt);
if (legacySource == null) {
return;
} else {
wssSource = legacySource;
}
}
// Guarantee that the wssSource value is a fully qualified URL
if (wssSource.indexOf("/") === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '');
if (location.protocol === 'https:') {
wssSource = "wss://" + base_part + wssSource;
} else if (location.protocol === 'http:') {
wssSource = "ws://" + base_part + wssSource;
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function () {
return htmx.createWebSocket(wssSource)
});
socketWrapper.addEventListener('message', function (event) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
var response = event.data;
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return;
}
api.withExtensions(socketElt, function (extension) {
response = extension.transformResponse(response, null, socketElt);
});
var settleInfo = api.makeSettleInfo(socketElt);
var fragment = api.makeFragment(response);
if (fragment.children.length) {
var children = Array.from(fragment.children);
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
}
}
api.settleImmediately(settleInfo.tasks);
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
});
// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(socketElt).webSocket = socketWrapper;
}
/**
* @typedef {Object} WebSocketWrapper
* @property {WebSocket} socket
* @property {Array<{message: string, sendElt: Element}>} messageQueue
* @property {number} retryCount
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {(message: string, sendElt: Element) => void} send
* @property {(event: string, handler: Function) => void} addEventListener
* @property {() => void} handleQueuedMessages
* @property {() => void} init
* @property {() => void} close
*/
/**
*
* @param socketElt
* @param socketFunc
* @returns {WebSocketWrapper}
*/
function createWebsocketWrapper(socketElt, socketFunc) {
var wrapper = {
socket: null,
messageQueue: [],
retryCount: 0,
/** @type {Object<string, Function[]>} */
events: {},
addEventListener: function (event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler);
}
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
},
sendImmediately: function (message, sendElt) {
if (!this.socket) {
api.triggerErrorEvent()
}
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message);
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message: message,
socketWrapper: this.publicInterface
})
}
},
send: function (message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message: message, sendElt: sendElt });
} else {
this.sendImmediately(message, sendElt);
}
},
handleQueuedMessages: function () {
while (this.messageQueue.length > 0) {
var queuedItem = this.messageQueue[0]
if (this.socket.readyState === this.socket.OPEN) {
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
this.messageQueue.shift();
} else {
break;
}
}
},
init: function () {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket
this.socket.close()
}
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc();
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
this.socket = socket;
socket.onopen = function (e) {
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
wrapper.handleQueuedMessages();
}
socket.onclose = function (e) {
// If socket should not be connected, stop further attempts to establish connection
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
setTimeout(function () {
wrapper.retryCount += 1;
wrapper.init();
}, delay);
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
};
socket.onerror = function (e) {
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
maybeCloseWebSocketSource(socketElt);
};
var events = this.events;
Object.keys(events).forEach(function (k) {
events[k].forEach(function (e) {
socket.addEventListener(k, e);
})
});
},
close: function () {
this.socket.close()
}
}
wrapper.init();
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
};
return wrapper;
}
/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
}
/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
}
/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} socketElt
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt);
var triggerSpecs = api.getTriggerSpecs(sendElt);
triggerSpecs.forEach(function (ts) {
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
/** @type {WebSocketWrapper} */
var socketWrapper = api.getInternalData(socketElt).webSocket;
var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
var results = api.getInputValues(sendElt, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(sendElt);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, sendElt);
var sendConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers: headers,
errors: errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
};
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return;
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors);
return;
}
var body = sendConfig.messageBody;
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend);
}
socketWrapper.send(body, elt);
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});
});
}
/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay;
if (typeof delay === 'function') {
return delay(retryCount);
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6);
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random();
}
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
}
/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close();
return true;
}
return false;
}
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, []);
sock.binaryType = htmx.config.wsBinaryType;
return sock;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
result.push(node)
})
return result
}
/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
})();

View File

@ -1,7 +1,7 @@
<div class="chat-container mt-5"
style="padding-bottom: 155px"
hx-indicator="#textarea-control"
hx-ext="sse">
hx-ext="ws">
<hx hx-get="/loadChat" hx-trigger="load once" hx-swap="outerHTML"></hx>
<hx hx-get="/loadChatInput" hx-trigger="load once" hx-swap="outerHTML" id="textarea-control"></hx>
</div>

View File

@ -12,7 +12,7 @@
<link rel="stylesheet" href="dependencies/russo.css">
<link rel="stylesheet" href="/style.css">
<script src="dependencies/htmx.js"></script>
<script src="dependencies/sse.js"></script>
<script src="dependencies/ws.js"></script>
<script src="dependencies/sortable.js"></script>
<script src="dependencies/pricing-table.js"></script>

View File

@ -5,9 +5,7 @@
<!-- Left column with the icon -->
{% if IsPlaceholder %}
<figure class="image is-48x48 message-icon"
style="flex-shrink: 0"
sse-swap="swapIcon-{{ ConversationAreaId }}">
<figure class="image is-48x48 message-icon" style="flex-shrink: 0">
<img src="icons/bouvai2.png"
alt="User Image"
id="selectedIcon-{{ ConversationAreaId }}">
@ -83,8 +81,7 @@
<div class="message-content"
id="content-{{ ConversationAreaId }}"
style="width: 100%;
overflow-y: hidden"
sse-swap="swapContent-{{ ConversationAreaId }}">
overflow-y: hidden">
<hx hx-trigger="load" hx-get="/generateMultipleMessages" id="generate-multiple-messages"></hx>
<div class='message-header'>
<p>Waiting...</p>
@ -109,9 +106,7 @@
{% for selectedLLM in SelectedLLMs %}
<button disable
class="button is-small is-primary message-button is-outlined mr-1"
sse-swap="swapSelectionBtn-{{ selectedLLM.ID.String() }}"
hx-swap="outerHTML"
hx-target="this">
id="selection-btn-{{ selectedLLM.ID.String() }}-{{ ConversationAreaId }}">
<span class="icon is-small">
<!--img src="icons/{{ selectedLLM.Company }}.png" alt="{{ selectedLLM.Name }}"
style="max-height: 100%; max-width: 100%;"-->

View File

@ -1,4 +1,5 @@
<button class="button is-small is-primary message-button is-outlined mr-1"
id="selection-btn-{{ message.LLMID }}-{{ ConversationAreaId }}"
hx-get="/messageContent?id={{ message.Id }}"
hx-target="#content-{{ ConversationAreaId }}"
onclick="updateIcon('{{ message.Icon }}', '{{ ConversationAreaId }}')"

View File

@ -145,7 +145,7 @@
type="button"
class="button is-small {% if not AnyExists %}is-danger{% endif %}">
<span class="icon is-small">
<i class="fa-solid fa-chevron-down"></i>
<i id="toggle-keys-icon" class="fa-solid fa-chevron-down"></i>
</span>
</button>
</p>
@ -223,7 +223,7 @@
// Do not take the id="save-field"
var fields = document.querySelectorAll("#api-keys-form .field.has-addons");
var saveButton = document.getElementById('save-keys-button');
var toggleIcon = this.querySelector('i');
var toggleIcon = document.getElementById('toggle-keys-icon');
fields.forEach(function (field) {
if (field.id == "save-field") return;

View File

@ -28,7 +28,7 @@
<p>For example, a response from GPT-4 Omni can be used by Claude Haiku.</p>
<a class="button is-primary mt-2 mb-2" href="/signin">Login</a>
<a class="button is-primary mt-2 mb-2" href="/signin">Try JADE now for free!</a>
<br />
<br />