Jade/Chat.go
2024-05-27 17:59:05 +02:00

862 lines
23 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net/url"
"sort"
"strings"
"time"
"github.com/edgedb/edgedb-go"
"github.com/flosch/pongo2"
"github.com/gofiber/fiber/v2"
)
func ChatPageHandler(c *fiber.Ctx) error {
authCookie := c.Cookies("jade-edgedb-auth-token", "")
fmt.Println("Main page")
if authCookie != "" && !checkIfLogin() {
edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie})
}
var (
isSub bool
limitReach bool
)
if !checkIfLogin() {
isSub = false
limitReach = false
} else {
isSub = IsCurrentUserSubscribed()
limitReach = IsCurrentUserLimiteReached()
}
return c.Render("chat", fiber.Map{"IsLogin": checkIfLogin(), "HaveKey": checkIfHaveKey(), "IsSubscribed": isSub, "IsLimiteReached": limitReach}, "layouts/main")
}
func DeleteMessageHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
messageUUID, err := edgedb.ParseUUID(messageId)
if err != nil {
fmt.Print("Error parsing UUID")
panic(err)
}
// Delete all messages
err = edgeClient.Execute(edgeCtx, `
WITH
messageArea := (SELECT Message FILTER .id = <uuid>$0).area
DELETE Area
FILTER .position >= messageArea.position AND .conversation = messageArea.conversation;
`, messageUUID)
if err != nil {
fmt.Print("Error deleting messages")
panic(err)
}
return c.SendString(generateChatHTML())
}
func LoadChatHandler(c *fiber.Ctx) error {
deleteLLMtoDelete()
if checkIfLogin() {
if IsCurrentUserLimiteReached() && !IsCurrentUserSubscribed() {
return c.SendString(generateLimitReachedChatHTML())
} else if !checkIfHaveKey() {
return c.SendString(generateEnterKeyChatHTML())
}
return c.SendString(generateChatHTML())
} else {
return c.SendString(generateWelcomeChatHTML())
}
}
type TemplateMessage struct {
Icon string
Content string
Hidden bool
Id string
Name string
Model string
ModelID string
}
func generateChatHTML() string {
// Print the name of the current conversation
var currentConv Conversation
err := edgeClient.QuerySingle(edgeCtx, `
SELECT global currentConversation { name }`, &currentConv)
if err != nil {
fmt.Print("Error getting current conversation")
panic(err)
}
fmt.Println("Current conversation: ", currentConv.Name)
// Maybe redo that to be area by area because look like shit rn. It come from early stage of dev. It work tho soooo...
var Messages []Message
err = edgeClient.Query(edgeCtx, `
SELECT Message {
id,
selected,
role,
content,
date,
llm : {
name,
modelInfo : {
modelID,
name,
company : {
icon
}
}
}
}
FILTER .conversation = global currentConversation AND .conversation.user = global currentUser
ORDER BY .date ASC
`, &Messages)
if err != nil {
fmt.Print("Error getting messages")
panic(err)
}
htmlString := "<div class='columns is-centered' id='chat-container'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
var templateMessages []TemplateMessage
for i, message := range Messages {
if message.Role == "user" {
htmlContent := markdownToHTML(message.Content)
userOut, err := userTmpl.Execute(pongo2.Context{"Content": htmlContent, "ID": message.ID.String()})
if err != nil {
fmt.Print("Error executing user template")
panic(err)
}
htmlString += userOut
// Reset NextMessages when a user message is encountered
templateMessages = []TemplateMessage{}
} else {
// For bot messages, add them to NextMessages with only the needed fields
templateMessage := TemplateMessage{
Icon: message.LLM.Model.Company.Icon, // Assuming Icon is a field you want to include from Message
Content: markdownToHTML(message.Content),
Hidden: !message.Selected, // Assuming Hidden is a field you want to include from Message
Id: message.ID.String(),
Name: message.LLM.Name,
Model: message.LLM.Model.Name,
ModelID: message.LLM.Model.ModelID,
}
templateMessages = append(templateMessages, templateMessage)
// Check if the next message is not a bot or if it's the last message
if i+1 == len(Messages) || Messages[i+1].Role != "bot" {
sort.Slice(templateMessages, func(i, j int) bool {
if !templateMessages[i].Hidden && templateMessages[j].Hidden {
return true
}
if templateMessages[i].Hidden && !templateMessages[j].Hidden {
return false
}
return true
})
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": templateMessages, "ConversationAreaId": i})
if err != nil {
fmt.Print("Error executing bot template")
panic(err)
}
htmlString += botOut
htmlString += "<div style='height: 10px;'></div>"
}
}
}
htmlString += "</div></div>"
// Render the HTML template with the messages
return htmlString
}
func GetUserMessageHandler(c *fiber.Ctx) error {
id := c.FormValue("id")
messageUUID, _ := edgedb.ParseUUID(id)
var selectedMessage Message
err := edgeClient.QuerySingle(context.Background(), `
SELECT Message {
content
}
FILTER
.id = <uuid>$0;
`, &selectedMessage, messageUUID)
if err != nil {
fmt.Print("Error getting message")
panic(err)
}
out, err := userTmpl.Execute(pongo2.Context{"Content": markdownToHTML(selectedMessage.Content), "ID": id})
if err != nil {
fmt.Print("Error executing user template")
panic(err)
}
return c.SendString(out)
}
func GetMessageContentHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
onlyContent := c.FormValue("onlyContent") // To init the text area of the edit message form
messageUUID, _ := edgedb.ParseUUID(messageId)
var selectedMessage Message
err := edgeClient.QuerySingle(context.Background(), `
SELECT Message {
content,
llm : {
name,
modelInfo : {
modelID,
name,
}
}
}
FILTER
.id = <uuid>$0;
`, &selectedMessage, messageUUID)
if err != nil {
panic(err)
}
if onlyContent == "true" {
return c.SendString(markdownToHTML(selectedMessage.Content))
}
out := "<div class='message-header'>"
out += "<p>"
out += "<strong>" + selectedMessage.LLM.Name + "</strong> <small>" + selectedMessage.LLM.Model.ModelID + "</small>"
out += " </p>"
out += "</div>"
out += "<div class='message-body'>"
out += " <ct class='content'>"
out += markdownToHTML(selectedMessage.Content)
out += " </ct>"
out += "</div>"
// Update the selected value of messages in the database
err = edgeClient.Execute(edgeCtx, `
WITH m := (SELECT Message FILTER .id = <uuid>$0)
UPDATE Message
FILTER .area = m.area
SET {selected := false};
`, messageUUID)
if err != nil {
panic(err)
}
_ = edgeClient.Execute(edgeCtx, `
UPDATE Message
FILTER .id = <uuid>$0
SET {selected := true};
`, messageUUID)
return c.SendString(out)
}
func generateWelcomeChatHTML() string {
welcomeMessage := `To start using JADE, please login.`
loginButton := `
<a class="button is-primary is-small" href="/signin">
Log in
</a>`
htmlString := "<div class='columns is-centered' id='chat-container'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
NextMessages := []TemplateMessage{}
nextMsg := TemplateMessage{
Icon: "icons/bouvai2.png", // Assuming Icon is a field you want to include from Message
Content: "<br>" + markdownToHTML(welcomeMessage) + loginButton,
Hidden: false, // Assuming Hidden is a field you want to include from Message
Id: "0",
Name: "JADE",
}
NextMessages = append(NextMessages, nextMsg)
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": true})
if err != nil {
fmt.Print("Error executing bot template")
panic(err)
}
htmlString += botOut
htmlString += "<div style='height: 10px;'></div>"
htmlString += "</div></div>"
// Render the HTML template with the messages
return htmlString
}
func generateEnterKeyChatHTML() string {
welcomeMessage := `
<p class="mt-2">To start using JADE, please enter at least one key in the settings.</p>
<ul>
<li>OpenAI: <a href="https://openai.com/index/openai-api" target="_blank">Link</a></li>
<li>Anthropic: <a href="https://www.anthropic.com/api" target="_blank">Link</a></li>
<li>Mistral: <a href="https://mistral.ai/news/la-plateforme" target="_blank">Link</a></li>
<li>Groq: <a href="https://console.groq.com/login" target="_blank">Link</a></li>
</ul>
`
htmlString := "<div class='columns is-centered' id='chat-container'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
NextMessages := []TemplateMessage{}
nextMsg := TemplateMessage{
Icon: "icons/bouvai2.png", // Assuming Icon is a field you want to include from Message
Content: welcomeMessage,
Hidden: false, // Assuming Hidden is a field you want to include from Message
Id: "0",
Name: "JADE",
}
NextMessages = append(NextMessages, nextMsg)
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": true})
if err != nil {
fmt.Print("Error executing bot template")
panic(err)
}
htmlString += botOut
htmlString += "<div style='height: 10px;'></div>"
htmlString += "</div></div>"
// Render the HTML template with the messages
return htmlString
}
func generateTermAndServiceHandler(c *fiber.Ctx) error {
return c.SendString(generateTermAndServiceChatHTML())
}
func generateTermAndServiceChatHTML() string {
welcomeMessage := `
<p class="mt-2">TODO Add terms and service.</p>
`
closeBtn := `
<div class="is-flex is-justify-content-flex-end">
<a class="button is-small is-danger is-outlined" hx-get="/loadChat" hx-target="#chat-container" hx-swap="outerHTML"
hx-trigger="click">
<span class="icon">
<i class="fa-solid fa-xmark"></i>
</span>
</a>
</div>`
htmlString := "<div class='columns is-centered' id='chat-container'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
NextMessages := []TemplateMessage{}
nextMsg := TemplateMessage{
Icon: "icons/bouvai2.png", // Assuming Icon is a field you want to include from Message
Content: welcomeMessage + closeBtn,
Hidden: false, // Assuming Hidden is a field you want to include from Message
Id: "0",
Name: "JADE",
}
NextMessages = append(NextMessages, nextMsg)
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": true})
if err != nil {
fmt.Print("Error executing bot template")
panic(err)
}
htmlString += botOut
htmlString += "<div style='height: 10px;'></div>"
htmlString += "</div></div>"
// Render the HTML template with the messages
return htmlString
}
func generateLimitReachedChatHTML() string {
welcomeMessage := `You have reached the maximum number of messages for a free account. Please upgrade your account to continue using JADE.`
var result User
err := edgeClient.QuerySingle(edgeCtx, "SELECT global currentUser { stripe_id, email } LIMIT 1;", &result)
if err != nil {
fmt.Print("Error getting current user")
panic(err)
}
clientSecretSession := CreateClientSecretSession()
// TODO Replace by live API call
stripeTable := `
<stripe-pricing-table
pricing-table-id="prctbl_1OxrazP2nW0okNQymYvskUk7"
publishable-key="pk_test_51OxXuWP2nW0okNQy2jyS70vx7WHZzDskvQazsitSDJQ3ifVHPqAkMv7orCePwRGRTarNn8uMuaxbVqD2Zg80oRc600epN4ycQ4"
customer-session-client-secret="` + clientSecretSession + `">
</stripe-pricing-table>`
htmlString := "<div class='columns is-centered' id='chat-container'><div class='column is-12-mobile is-8-tablet is-6-desktop' id='chat-messages'>"
NextMessages := []TemplateMessage{}
nextMsg := TemplateMessage{
Icon: "icons/bouvai2.png", // Assuming Icon is a field you want to include from Message
Content: "<br>" + markdownToHTML(welcomeMessage) + "<br>" + stripeTable,
Hidden: false, // Assuming Hidden is a field you want to include from Message
Id: "0",
Name: "JADE",
}
NextMessages = append(NextMessages, nextMsg)
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": true})
if err != nil {
fmt.Print("Error executing bot template")
panic(err)
}
htmlString += botOut
htmlString += "<div style='height: 10px;'></div>"
htmlString += "</div></div>"
// Render the HTML template with the messages
return htmlString
}
// Buton actions
func GetEditMessageFormHandler(c *fiber.Ctx) error {
id := c.FormValue("id")
idUUID, _ := edgedb.ParseUUID(id)
var message Message
err := edgeClient.QuerySingle(context.Background(), `
SELECT Message { content }
FILTER .id = <uuid>$0;
`, &message, idUUID)
if err != nil {
fmt.Print("Error getting message")
panic(err)
}
// Calculate the number of rows based on the length of the content
rows := len(strings.Split(message.Content, "\n"))
if rows < 10 {
rows = 10
}
tmpl := pongo2.Must(pongo2.FromFile("views/partials/message-edit-form.html"))
out, err := tmpl.Execute(pongo2.Context{"Content": message.Content, "ID": id, "Rows": rows})
if err != nil {
fmt.Print("Error executing user template")
panic(err)
}
return c.SendString(out)
}
func RedoMessageHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
messageUUID, _ := edgedb.ParseUUID(messageId)
var selectedLLMIds []string
err := json.Unmarshal([]byte(c.FormValue("selectedLLMIds")), &selectedLLMIds)
if err != nil {
fmt.Print("Error unmarshalling selected LLM IDs")
panic(err)
}
var message Message
err = edgeClient.QuerySingle(context.Background(), `
SELECT Message { content }
FILTER .id = <uuid>$0;
`, &message, messageUUID)
if err != nil {
fmt.Print("Error getting message")
panic(err)
}
// Delete messages
err = edgeClient.Execute(edgeCtx, `
WITH
messageArea := (SELECT Message FILTER .id = <uuid>$0).area
DELETE Area
FILTER .position > messageArea.position AND .conversation = messageArea.conversation AND .conversation.user = global currentUser;
`, messageUUID)
if err != nil {
fmt.Print("Error deleting messages")
panic(err)
}
return c.SendString(GeneratePlaceholderHTML(message.Content, selectedLLMIds, false))
}
func EditMessageHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
message := c.FormValue("message")
messageUUID, _ := edgedb.ParseUUID(messageId)
var selectedLLMIds []string
err := json.Unmarshal([]byte(c.FormValue("selectedLLMIds")), &selectedLLMIds)
if err != nil {
fmt.Print("Error unmarshalling selected LLM IDs")
panic(err)
}
if len(selectedLLMIds) == 0 {
return c.SendString("")
}
// Delete messages
err = edgeClient.Execute(edgeCtx, `
WITH
messageArea := (SELECT Message FILTER .id = <uuid>$0).area
DELETE Area
FILTER .position >= messageArea.position AND .conversation = messageArea.conversation AND .conversation.user = global currentUser;
`, messageUUID)
if err != nil {
fmt.Print("Error deleting messages")
panic(err)
}
return c.SendString(GeneratePlaceholderHTML(message, selectedLLMIds, true))
}
func ClearChatHandler(c *fiber.Ctx) error {
// Delete the default conversation
err := edgeClient.Execute(edgeCtx, `
DELETE Area
FILTER .conversation = global currentConversation;
`)
if err != nil {
fmt.Print("Error deleting messages")
panic(err)
}
return c.SendString(generateChatHTML())
}
// Popover stuff
func LoadUsageKPIHandler(c *fiber.Ctx) error {
if !checkIfLogin() || !checkIfHaveKey() {
return c.SendString("")
}
InputDateID := c.FormValue("month", time.Now().Format("01-2006"))
offset := c.FormValue("offset")
IsActive := false
var InputDate time.Time
InputDate, err := time.Parse("01-2006", InputDateID)
if err != nil {
fmt.Print("Error parsing date")
panic(err)
}
if offset == "-1" {
InputDate = InputDate.AddDate(0, -1, 0)
IsActive = true
} else if offset == "1" {
InputDate = InputDate.AddDate(0, 1, 0)
IsActive = true
}
type UsageKPI struct {
Key struct {
ModelID string `edgedb:"model_id"`
} `edgedb:"key"`
TotalCost float32 `edgedb:"total_cost"`
TotalCount int64 `edgedb:"total_count"`
}
var usages []UsageKPI
err = edgeClient.Query(edgeCtx, `
WITH
U := (
SELECT Usage
FILTER .user = global currentUser and .date >= <datetime>$0 AND .date < <datetime>$1
),
grouped := (
GROUP U {
model_id,
input_cost,
output_cost,
} BY .model_id
)
SELECT grouped {
key := .key { model_id },
total_count := count(.elements),
total_cost := sum(.elements.input_cost) + sum(.elements.output_cost),
} FILTER .total_count > 0 ORDER BY .total_cost DESC
`, &usages, InputDate, InputDate.AddDate(0, 1, 0))
if err != nil {
fmt.Print("Error getting usage")
panic(err)
}
BeautifullDate := InputDate.Format("Jan 2006")
var (
TotalCount int64
TotalCost float32
)
for _, usage := range usages {
TotalCost += usage.TotalCost
TotalCount += usage.TotalCount
}
out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-usage.html")).Execute(pongo2.Context{
"usages": usages,
"TotalCost": TotalCost,
"TotalCount": TotalCount,
"Date": BeautifullDate,
"DateID": InputDate.Format("01-2006"),
"IsActive": IsActive,
})
if err != nil {
fmt.Print("Error generating usage")
panic(err)
}
return c.SendString(out)
}
func LoadKeysHandler(c *fiber.Ctx) error {
if !checkIfLogin() {
return c.SendString("")
}
openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists := getExistingKeys()
out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-keys.html")).Execute(pongo2.Context{
"IsLogin": checkIfLogin(),
"OpenaiExists": openaiExists,
"AnthropicExists": anthropicExists,
"MistralExists": mistralExists,
"GroqExists": groqExists,
"GooseaiExists": gooseaiExists,
"GoogleExists": googleExists,
"AnyExists": openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists,
})
if err != nil {
fmt.Print("Error loading keys")
panic(err)
}
return c.SendString(out)
}
func GenerateModelPopoverHTML(refresh bool) string {
openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists := getExistingKeys()
var llms []LLM
err := edgeClient.Query(edgeCtx, `
SELECT LLM {
id,
name,
context,
temperature,
modelInfo : {
modelID,
name,
company : {
name,
icon
}
}
}
FILTER .user = global currentUser AND .name != 'none' AND .to_delete = false
ORDER BY .position
`, &llms)
if err != nil {
fmt.Print("Error loading LLMs")
panic(err)
}
modelInfos := GetAvailableModels()
out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-models.html")).Execute(pongo2.Context{
"IsLogin": checkIfLogin(),
"OpenaiExists": openaiExists,
"AnthropicExists": anthropicExists,
"MistralExists": mistralExists,
"GroqExists": groqExists,
"GooseaiExists": gooseaiExists,
"GoogleExists": googleExists,
"AnyExists": openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists,
"LLMs": llms,
"ModelInfos": modelInfos,
"DeleteUpdate": refresh,
"IsSub": IsCurrentUserSubscribed(),
})
if err != nil {
fmt.Print("Error generating model popover")
panic(err)
}
return out
}
func LoadModelSelectionHandler(c *fiber.Ctx) error {
if !checkIfLogin() || !checkIfHaveKey() {
return c.SendString("")
}
return c.SendString(GenerateModelPopoverHTML(false))
}
func GenerateConversationPopoverHTML(isActive bool) string {
var conversations []Conversation
err := edgeClient.Query(edgeCtx, `
SELECT Conversation {
name,
position,
id
}
FILTER .user = global currentUser
ORDER BY .position
`, &conversations)
if err != nil {
fmt.Print("Error loading conversations")
panic(err)
}
out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-conversation.html")).Execute(pongo2.Context{
"Conversations": conversations,
"IsActive": isActive,
})
if err != nil {
fmt.Print("Error generating conversation popover")
panic(err)
}
return out
}
func LoadConversationSelectionHandler(c *fiber.Ctx) error {
if !checkIfLogin() || !checkIfHaveKey() {
return c.SendString("")
}
return c.SendString(GenerateConversationPopoverHTML(false))
}
func RefreshConversationSelectionHandler(c *fiber.Ctx) error {
IsActive := c.FormValue("IsActive") == "true"
return c.SendString(GenerateConversationPopoverHTML(!IsActive))
}
func LoadSettingsHandler(c *fiber.Ctx) error {
if !checkIfLogin() {
return c.SendString("")
}
var user User
err := edgeClient.QuerySingle(edgeCtx, `
SELECT User {
email
}
FILTER .id = global currentUser.id
`, &user)
if err != nil {
fmt.Print("Error loading user")
panic(err)
}
// Percent encoding of the email
user.Email = url.QueryEscape(user.Email)
stripeSubLink := "https://billing.stripe.com/p/login/test_eVa5kC1q7dogaaIcMM?prefilled_email=" + user.Email
openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists := getExistingKeys()
out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-settings.html")).Execute(pongo2.Context{
"IsLogin": checkIfLogin(),
"OpenaiExists": openaiExists,
"AnthropicExists": anthropicExists,
"MistralExists": mistralExists,
"GroqExists": groqExists,
"GooseaiExists": gooseaiExists,
"GoogleExists": googleExists,
"AnyExists": openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists,
"IsSub": IsCurrentUserSubscribed(),
"StripeSubLink": stripeSubLink,
})
if err != nil {
fmt.Print("Error loading settings")
panic(err)
}
return c.SendString(out)
}
func CreateConversationHandler(c *fiber.Ctx) error {
name := c.FormValue("conversation-name-input")
if name == "" {
name = "New Conversation"
}
err := edgeClient.Execute(edgeCtx, `
WITH
C := (
SELECT Conversation
FILTER .user = global currentUser
)
INSERT Conversation {
name := <str>$0,
user := global currentUser,
position := count(C) + 1
}
`, name)
if err != nil {
fmt.Print("Error creating conversation")
panic(err)
}
return c.SendString(GenerateConversationPopoverHTML(true))
}
func DeleteConversationHandler(c *fiber.Ctx) error {
conversationId := c.FormValue("conversationId")
conversationUUID, err := edgedb.ParseUUID(conversationId)
if err != nil {
fmt.Print("Error parsing UUID")
panic(err)
}
err = edgeClient.Execute(edgeCtx, `
DELETE Conversation
FILTER .user = global currentUser AND .id = <uuid>$0;
`, conversationUUID)
if err != nil {
fmt.Print("Error deleting conversation")
panic(err)
}
return c.SendString(GenerateConversationPopoverHTML(true))
}
func SelectConversationHandler(c *fiber.Ctx) error {
conversationId := c.FormValue("conversation-id")
conversationUUID, err := edgedb.ParseUUID(conversationId)
if err != nil {
fmt.Print("Error parsing UUID")
panic(err)
}
fmt.Println("conversationUUID", conversationUUID.String())
err = edgeClient.Execute(edgeCtx, `
SET global currentConversation := (
SELECT Conversation
FILTER .id = <uuid>$0
LIMIT 1
);
`, conversationUUID)
if err != nil {
fmt.Print("Error selecting conversation")
panic(err)
}
return c.SendString(generateChatHTML())
}