diff --git a/Chat.go b/Chat.go index ff33888..0f85b5a 100644 --- a/Chat.go +++ b/Chat.go @@ -15,11 +15,6 @@ import ( func ChatPageHandler(c *fiber.Ctx) error { authCookie := c.Cookies("jade-edgedb-auth-token", "") - go func() { - fmt.Println("Sending test event") - sseChanel.SendEvent("test", "Hello from server") - }() - if authCookie != "" && !checkIfLogin() { edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}) } @@ -141,19 +136,16 @@ func generateChatHTML() string { func GetMessageContentHandler(c *fiber.Ctx) error { messageId := c.FormValue("id") - onlyContent := c.FormValue("onlyContent") + onlyContent := c.FormValue("onlyContent") // To init the text area of the edit message form - messageUUID, err := edgedb.ParseUUID(messageId) - if err != nil { - fmt.Println("Error in uuid.FromString: in DeleteMessageHandler") - fmt.Println(err) - } + messageUUID, _ := edgedb.ParseUUID(messageId) var selectedMessage Message - err = edgeClient.QuerySingle(context.Background(), ` + err := edgeClient.QuerySingle(context.Background(), ` SELECT Message { model_id, - content + content, + area } FILTER .id = $0; @@ -162,12 +154,11 @@ func GetMessageContentHandler(c *fiber.Ctx) error { log.Fatal(err) } - modelID, _ := selectedMessage.ModelID.Get() - if onlyContent == "true" { return c.SendString(markdownToHTML(selectedMessage.Content)) } + modelID, _ := selectedMessage.ModelID.Get() out := "
" out += "

" out += model2Name(modelID) @@ -303,15 +294,6 @@ func RedoMessageHandler(c *fiber.Ctx) error { }) } - selectedModelIds := []string{} - for ModelInfo := range ModelsInfos { - out := c.FormValue("model-check-" + ModelsInfos[ModelInfo].ID) - if out != "" { - selectedModelIds = append(selectedModelIds, ModelsInfos[ModelInfo].ID) - } - } - lastSelectedModelIds = removeDuplicate(selectedModelIds) - return c.SendString(messageOut) } diff --git a/Request.go b/Request.go index 865913c..e5ecf90 100644 --- a/Request.go +++ b/Request.go @@ -28,62 +28,69 @@ type CompanyInfo struct { ModelInfos []ModelInfo } +type SelectedModel struct { + ID string + Name string + Icon string +} + var CompanyInfos []CompanyInfo var ModelsInfos []ModelInfo -type MultipleModelsCompletionRequest struct { - ModelIds []string - Messages []Message - Message string -} - -type BotContentMessage struct { - Content string - Hidden bool -} - -var lastSelectedModelIds []string - -func addUsage(inputCost float32, outputCost float32, inputToken int32, outputToken int32, modelID string) { - // Create a new usage - err := edgeClient.Execute(edgeCtx, ` - INSERT Usage { - input_cost := $0, - output_cost := $1, - input_token := $2, - output_token := $3, - model_id := $4, - user := global currentUser - } - `, inputCost, outputCost, inputToken, outputToken, modelID) - if err != nil { - fmt.Println("Error in edgedb.QuerySingle: in addUsage") - log.Fatal(err) - } -} - func GenerateMultipleMessages(c *fiber.Ctx) error { + message := c.FormValue("message", "") + selectedModelIds := []string{} + for ModelInfo := range ModelsInfos { + out := c.FormValue("model-check-" + ModelsInfos[ModelInfo].ID) + if out != "" { + selectedModelIds = append(selectedModelIds, ModelsInfos[ModelInfo].ID) + } + } + + _, position := insertArea() + messageID := insertUserMessage(message) + + out := "" + messageOut, _ := userTmpl.Execute(pongo2.Context{"Content": markdownToHTML(message), "ID": messageID.String()}) + out += messageOut + + var selectedModels []SelectedModel + for i := range selectedModelIds { + selectedModels = append(selectedModels, SelectedModel{ID: selectedModelIds[i], Name: model2Name(selectedModelIds[i]), Icon: model2Icon(selectedModelIds[i])}) + } + + messageOut, _ = botTmpl.Execute(pongo2.Context{"IsPlaceholder": true, "selectedModels": selectedModels, "ConversationAreaId": position + 1}) + out += messageOut + + go HandleGenerateMultipleMessages(selectedModelIds) + + return c.SendString(out) +} + +func HandleGenerateMultipleMessages(selectedModelIds []string) { insertArea() // Create a wait group to synchronize the goroutines var wg sync.WaitGroup - // Add the length of lastSelectedModelIds goroutines to the wait group - wg.Add(len(lastSelectedModelIds)) + // Add the length of selectedModelIds goroutines to the wait group + wg.Add(len(selectedModelIds)) - for i := range lastSelectedModelIds { + // Create a channel to receive the index of the first completed goroutine + firstDone := make(chan int, 1) + + for i := range selectedModelIds { idx := i go func() { + defer wg.Done() + // Create a context with a 1-minute timeout ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() // Ensure the context is cancelled to free resources - // Use a channel to signal the completion of addxxxMessage - done := make(chan struct{}, 1) - // Determine which message function to call based on the model var addMessageFunc func(modelID string, selected bool) edgedb.UUID - switch model2Icon(lastSelectedModelIds[idx]) { + switch model2Icon(selectedModelIds[idx]) { case "openai": addMessageFunc = addOpenaiMessage case "anthropic": @@ -94,62 +101,110 @@ func GenerateMultipleMessages(c *fiber.Ctx) error { addMessageFunc = addGroqMessage } - // Call the selected addMessageFunc in a goroutine - go func() { - defer wg.Done() - if addMessageFunc != nil { - addMessageFunc(lastSelectedModelIds[idx], idx == 0) - } - done <- struct{}{} - }() + var messageID edgedb.UUID + if addMessageFunc != nil { + messageID = addMessageFunc(selectedModelIds[idx], idx == 0) + } - // Use select to wait on multiple channel operations + var message Message + err := edgeClient.QuerySingle(edgeCtx, ` + SELECT Message { + model_id, + content, + area + } + FILTER .id = $0; + `, &message, messageID) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in GenerateMultipleMessages") + log.Fatal(err) + } + modelID, _ := message.ModelID.Get() + + var area Area + err = edgeClient.QuerySingle(edgeCtx, ` + SELECT Area { + position + } + FILTER .id = $0; + `, &area, message.Area.ID) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in GenerateMultipleMessages") + log.Fatal(err) + } + + fmt.Println(area) + + // Check if the context's deadline is exceeded select { - case <-ctx.Done(): // Context's deadline is exceeded - // Insert a bot message indicating a timeout - insertBotMessage(lastSelectedModelIds[idx]+" too long to answer", idx == 0, lastSelectedModelIds[idx]) - case <-done: // addMessageFunc completed within the deadline - // No action needed, the function completed successfully + 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: + // Generate the HTML content + out := "

" + out += "

" + out += model2Name(modelID) + out += "

" + out += "
" + out += "
" + out += " " + out += markdownToHTML(message.Content) + out += " " + out += "
" + + fmt.Println("Sending event from first") + fmt.Println("swapContent-" + fmt.Sprintf("%d", area.Position)) + + // Send Content event + sseChanel.SendEvent( + "swapContent-"+fmt.Sprintf("%d", area.Position), + out, + ) + + out, err := modelSelecBtnTmpl.Execute(map[string]interface{}{ + "message": message, + }) + if err != nil { + fmt.Println("Error in modelSelecBtnTmpl.Execute: in GenerateMultipleMessages") + log.Fatal(err) + } + + // Send Content event + sseChanel.SendEvent( + "swapSelectionBtn-"+modelID, + out, + ) + + // Send Icon Swap event + sseChanel.SendEvent( + "swapIcon-"+fmt.Sprintf("%d", area.Position), + `User Image`, + ) + default: + out, err := modelSelecBtnTmpl.Execute(map[string]interface{}{ + "message": message, + }) + if err != nil { + fmt.Println("Error in modelSelecBtnTmpl.Execute: in GenerateMultipleMessages") + log.Fatal(err) + } + + fmt.Println(("Sending event")) + + // Send Content event + sseChanel.SendEvent( + "swapSelectionBtn-"+modelID, + out, + ) + } } }() } // Wait for all goroutines to finish wg.Wait() - - fmt.Println("Done!") - - return c.SendString(generateChatHTML()) -} - -func RequestMultipleMessagesHandler(c *fiber.Ctx) error { - message := c.FormValue("message", "") - - selectedModelIds := []string{} - for ModelInfo := range ModelsInfos { - out := c.FormValue("model-check-" + ModelsInfos[ModelInfo].ID) - if out != "" { - selectedModelIds = append(selectedModelIds, ModelsInfos[ModelInfo].ID) - } - } - lastSelectedModelIds = selectedModelIds - - out := RequestMultipleMessages(message, selectedModelIds) - - return c.SendString(out) -} - -func RequestMultipleMessages(message string, selectedModelIds []string) string { - // Add an Area with the user message inside - insertArea() - messageID := insertUserMessage(message) - - out := "" - messageOut, _ := userTmpl.Execute(pongo2.Context{"Content": markdownToHTML(message), "ID": messageID.String()}) - out += messageOut - - messageOut, _ = botTmpl.Execute(pongo2.Context{"IsPlaceholder": true}) - out += messageOut - - return out } diff --git a/RequestAnthropic.go b/RequestAnthropic.go index 3c8ee85..73b9d05 100644 --- a/RequestAnthropic.go +++ b/RequestAnthropic.go @@ -11,15 +11,10 @@ import ( ) type AnthropicChatCompletionRequest struct { - Model string `json:"model"` - Messages []AnthropicMessage `json:"messages"` - MaxTokens int `json:"max_tokens"` - Temperature float64 `json:"temperature"` -} - -type AnthropicMessage struct { - Role string `json:"role"` - Content string `json:"content"` + Model string `json:"model"` + Messages []Message `json:"messages"` + MaxTokens int `json:"max_tokens"` + Temperature float64 `json:"temperature"` } type AnthropicChatCompletionResponse struct { @@ -104,30 +99,10 @@ func addAnthropicMessage(modelID string, selected bool) edgedb.UUID { return edgedb.UUID{} } -func EdgeMessages2AnthropicMessages(messages []Message) []AnthropicMessage { - AnthropicMessages := make([]AnthropicMessage, len(messages)) - for i, msg := range messages { - var role string - switch msg.Role { - case "user": - role = "user" - case "bot": - role = "assistant" - default: - role = "system" - } - AnthropicMessages[i] = AnthropicMessage{ - Role: role, - Content: msg.Content, - } - } - return AnthropicMessages -} - func TestAnthropicKey(apiKey string) bool { url := "https://api.anthropic.com/v1/messages" - AnthropicMessages := []AnthropicMessage{ + AnthropicMessages := []Message{ { Role: "user", Content: "Hello", @@ -195,11 +170,9 @@ func RequestAnthropic(model string, messages []Message, maxTokens int, temperatu url := "https://api.anthropic.com/v1/messages" - AnthropicMessages := EdgeMessages2AnthropicMessages(messages) - requestBody := AnthropicChatCompletionRequest{ Model: model, - Messages: AnthropicMessages, + Messages: ChangeRoleBot2Assistant(messages), MaxTokens: maxTokens, Temperature: temperature, } diff --git a/RequestGroq.go b/RequestGroq.go index 6327665..8f6597d 100644 --- a/RequestGroq.go +++ b/RequestGroq.go @@ -11,14 +11,9 @@ import ( ) type GroqChatCompletionRequest struct { - Model string `json:"model"` - Messages []GroqMessage `json:"messages"` - Temperature float64 `json:"temperature"` -} - -type GroqMessage struct { - Role string `json:"role"` - Content string `json:"content"` + Model string `json:"model"` + Messages []Message `json:"messages"` + Temperature float64 `json:"temperature"` } type GroqChatCompletionResponse struct { @@ -37,9 +32,9 @@ type GroqUsage struct { } type GroqChoice struct { - Message GroqMessage `json:"message"` - FinishReason string `json:"finish_reason"` - Index int `json:"index"` + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` } func init() { @@ -106,31 +101,11 @@ func addGroqMessage(modelID string, selected bool) edgedb.UUID { return edgedb.UUID{} } -func EdgeMessages2GroqMessages(messages []Message) []GroqMessage { - groqMessages := make([]GroqMessage, len(messages)) - for i, msg := range messages { - var role string - switch msg.Role { - case "user": - role = "user" - case "bot": - role = "assistant" - default: - role = "system" - } - groqMessages[i] = GroqMessage{ - Role: role, - Content: msg.Content, - } - } - return groqMessages -} - func TestGroqKey(apiKey string) bool { url := "https://api.groq.com/openai/v1/chat/completions" // Convert messages to Qroq format - groqMessages := []GroqMessage{ + groqMessages := []Message{ { Role: "user", Content: "Hello", @@ -199,12 +174,9 @@ func RequestGroq(model string, messages []Message, temperature float64) (GroqCha url := "https://api.groq.com/openai/v1/chat/completions" - // Convert messages to Qroq format - groqMessages := EdgeMessages2GroqMessages(messages) - requestBody := GroqChatCompletionRequest{ Model: model, - Messages: groqMessages, + Messages: ChangeRoleBot2Assistant(messages), Temperature: temperature, } diff --git a/RequestMistral.go b/RequestMistral.go index dc72b22..0b44847 100644 --- a/RequestMistral.go +++ b/RequestMistral.go @@ -11,16 +11,10 @@ import ( ) type MistralChatCompletionRequest struct { - Model string `json:"model"` - Messages []MistralMessage `json:"messages"` - Temperature float64 `json:"temperature"` + Model string `json:"model"` + Messages []Message `json:"messages"` + Temperature float64 `json:"temperature"` } - -type MistralMessage struct { - Role string `json:"role"` - Content string `json:"content"` -} - type MistralChatCompletionResponse struct { ID string `json:"id"` Object string `json:"object"` @@ -37,9 +31,9 @@ type MistralUsage struct { } type MistralChoice struct { - Message MistralMessage `json:"message"` - FinishReason string `json:"finish_reason"` - Index int `json:"index"` + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` } func init() { @@ -139,31 +133,11 @@ func addMistralMessage(modelID string, selected bool) edgedb.UUID { return edgedb.UUID{} } -func EdgeMessages2MistralMessages(messages []Message) []MistralMessage { - mistralMessages := make([]MistralMessage, len(messages)) - for i, msg := range messages { - var role string - switch msg.Role { - case "user": - role = "user" - case "bot": - role = "assistant" - default: - role = "system" - } - mistralMessages[i] = MistralMessage{ - Role: role, - Content: msg.Content, - } - } - return mistralMessages -} - func TestMistralKey(apiKey string) bool { url := "https://api.mistral.ai/v1/chat/completions" // Convert messages to Mistral format - mistralMessages := []MistralMessage{ + mistralMessages := []Message{ { Role: "user", Content: "Hello", @@ -237,12 +211,9 @@ func RequestMistral(model string, messages []Message, temperature float64) (Mist url := "https://api.mistral.ai/v1/chat/completions" - // Convert messages to OpenAI format - mistralMessages := EdgeMessages2MistralMessages(messages) - requestBody := MistralChatCompletionRequest{ Model: model, - Messages: mistralMessages, + Messages: ChangeRoleBot2Assistant(messages), Temperature: temperature, } diff --git a/RequestOpenai.go b/RequestOpenai.go index 0476a0e..c4962cb 100644 --- a/RequestOpenai.go +++ b/RequestOpenai.go @@ -11,14 +11,9 @@ import ( ) type OpenaiChatCompletionRequest struct { - Model string `json:"model"` - Messages []OpenaiMessage `json:"messages"` - Temperature float64 `json:"temperature"` -} - -type OpenaiMessage struct { - Role string `json:"role"` - Content string `json:"content"` + Model string `json:"model"` + Messages []Message `json:"messages"` + Temperature float64 `json:"temperature"` } type OpenaiChatCompletionResponse struct { @@ -37,9 +32,9 @@ type OpenaiUsage struct { } type OpenaiChoice struct { - Message OpenaiMessage `json:"message"` - FinishReason string `json:"finish_reason"` - Index int `json:"index"` + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` } func init() { @@ -117,31 +112,11 @@ func addOpenaiMessage(modelID string, selected bool) edgedb.UUID { return edgedb.UUID{} } -func EdgeMessages2OpenaiMessages(messages []Message) []OpenaiMessage { - openaiMessages := make([]OpenaiMessage, len(messages)) - for i, msg := range messages { - var role string - switch msg.Role { - case "user": - role = "user" - case "bot": - role = "assistant" - default: - role = "system" - } - openaiMessages[i] = OpenaiMessage{ - Role: role, - Content: msg.Content, - } - } - return openaiMessages -} - func TestOpenaiKey(apiKey string) bool { url := "https://api.openai.com/v1/chat/completions" // Convert messages to OpenAI format - openaiMessages := []OpenaiMessage{ + openaiMessages := []Message{ { Role: "user", Content: "Hello", @@ -208,12 +183,9 @@ func RequestOpenai(model string, messages []Message, temperature float64) (Opena url := "https://api.openai.com/v1/chat/completions" - // Convert messages to OpenAI format - openaiMessages := EdgeMessages2OpenaiMessages(messages) - requestBody := OpenaiChatCompletionRequest{ Model: model, - Messages: openaiMessages, + Messages: ChangeRoleBot2Assistant(messages), Temperature: temperature, } diff --git a/database.go b/database.go index 16955bb..52ec80c 100644 --- a/database.go +++ b/database.go @@ -40,7 +40,7 @@ type Conversation struct { type Area struct { ID edgedb.UUID `edgedb:"id"` - Position int `edgedb:"position"` + Position int64 `edgedb:"position"` Conv Conversation `edgedb:"conversation"` } @@ -111,6 +111,24 @@ func checkIfLogin() bool { return err == nil } +func addUsage(inputCost float32, outputCost float32, inputToken int32, outputToken int32, modelID string) { + // Create a new usage + err := edgeClient.Execute(edgeCtx, ` + INSERT Usage { + input_cost := $0, + output_cost := $1, + input_token := $2, + output_token := $3, + model_id := $4, + user := global currentUser + } + `, inputCost, outputCost, inputToken, outputToken, modelID) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addUsage") + log.Fatal(err) + } +} + func insertNewConversation() edgedb.UUID { var inserted struct{ id edgedb.UUID } err := edgeClient.QuerySingle(edgeCtx, ` @@ -126,7 +144,7 @@ func insertNewConversation() edgedb.UUID { return inserted.id } -func insertArea() edgedb.UUID { +func insertArea() (edgedb.UUID, int64) { // If the Default conversation doesn't exist, create it. var convExists bool edgeClient.QuerySingle(edgeCtx, ` @@ -157,7 +175,21 @@ func insertArea() edgedb.UUID { log.Fatal(err) } - return inserted.id + var positionSet struct{ position int64 } + err = edgeClient.QuerySingle(edgeCtx, ` + SELECT Area { + position + } + FILTER .conversation.name = 'Default' AND .conversation.user = global currentUser + ORDER BY .position + LIMIT 1 + `, &positionSet) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in insertArea") + log.Fatal(err) + } + + return inserted.id, positionSet.position } func insertUserMessage(content string) edgedb.UUID { diff --git a/main.go b/main.go index d815a40..4dc4db6 100644 --- a/main.go +++ b/main.go @@ -12,11 +12,13 @@ import ( var userTmpl *pongo2.Template var botTmpl *pongo2.Template +var modelSelecBtnTmpl *pongo2.Template var sseChanel *ssefiber.FiberSSEChannel func main() { botTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-bot.html")) userTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-user.html")) + modelSelecBtnTmpl = pongo2.Must(pongo2.FromFile("views/partials/model-selection-btn.html")) // Import HTML using django engine/template engine := django.New("./views", ".html") @@ -28,7 +30,7 @@ func main() { EnablePrintRoutes: true, }) sse := ssefiber.New(app, "/sse") - sseChanel = sse.CreateChannel("sse", "/sse") + sseChanel = sse.CreateChannel("sse", "") // Add default logger app.Use(logger.New()) @@ -41,7 +43,6 @@ func main() { app.Get("/loadChat", LoadChatHandler) // Chat routes - app.Post("/requestMultipleMessages", RequestMultipleMessagesHandler) app.Post("/deleteMessage", DeleteMessageHandler) app.Get("/generateMultipleMessages", GenerateMultipleMessages) app.Get("/messageContent", GetMessageContentHandler) @@ -66,6 +67,10 @@ func main() { app.Get("/test", func(c *fiber.Ctx) error { fmt.Println("Hello from test") + go sseChanel.SendEvent( + "swapIcon-1", + `User Image`, + ) return c.SendString("") }) diff --git a/static/My logo - background (1).png b/static/My logo - background (1).png new file mode 100644 index 0000000..1c6b921 Binary files /dev/null and b/static/My logo - background (1).png differ diff --git a/static/style.css b/static/style.css index b3ce253..2f83bc1 100644 --- a/static/style.css +++ b/static/style.css @@ -47,10 +47,6 @@ html { --bulma-primary: #126d0f; } -.button.is-primary { - background-color: #1ebc19; -} - /* Chat input stuff */ .chat-input-container { display: flex; diff --git a/utils.go b/utils.go index a78c717..0cd6fdf 100644 --- a/utils.go +++ b/utils.go @@ -124,3 +124,23 @@ func removeDuplicate(s []string) []string { } return result } + +func ChangeRoleBot2Assistant(messages []Message) []Message { + openaiMessages := make([]Message, len(messages)) + for i, msg := range messages { + var role string + switch msg.Role { + case "user": + role = "user" + case "bot": + role = "assistant" + default: + role = "system" + } + openaiMessages[i] = Message{ + Role: role, + Content: msg.Content, + } + } + return openaiMessages +} diff --git a/views/chat.html b/views/chat.html index 038af50..c090282 100644 --- a/views/chat.html +++ b/views/chat.html @@ -18,8 +18,13 @@ + {% endfor %} {% endif %} +
- {% elif IsPlaceholder %} - - - -
+ {% elif IsPlaceholder %} +
+
- +
- {% endif %}
+ +
+ {% for selectedModel in selectedModels %} + + {% endfor %} +
+ {% endif %}
+ \ No newline at end of file diff --git a/views/partials/model-selection-btn.html b/views/partials/model-selection-btn.html new file mode 100644 index 0000000..0c64e58 --- /dev/null +++ b/views/partials/model-selection-btn.html @@ -0,0 +1,7 @@ + \ No newline at end of file