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:
parent
c7924476ee
commit
3266c8787e
123
Chat.go
123
Chat.go
@ -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,
|
||||
|
18
MyUtils.go
18
MyUtils.go
@ -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 {
|
||||
|
125
Request.go
125
Request.go
@ -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
32
go.mod
@ -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
36
go.sum
@ -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
91
main.go
@ -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
476
static/dependencies/ws.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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%;"-->
|
||||
|
@ -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 }}')"
|
||||
|
@ -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;
|
||||
|
@ -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 />
|
||||
|
Loading…
x
Reference in New Issue
Block a user