Jade/Chat.go

1097 lines
34 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 {
return c.Render("chat", fiber.Map{}, "layouts/main")
}
func LoadChatHandler(c *fiber.Ctx) error {
deleteLLMtoDelete(c)
if checkIfLogin(c) {
isPremium, isBasic := IsCurrentUserSubscribed(c)
if IsCurrentUserLimiteReached(c, isBasic) && !isPremium {
return c.SendString(generateLimitReachedChatHTML(c))
} else if !checkIfHaveKey(c) {
return c.SendString(generateEnterKeyChatHTML())
}
return c.SendString(generateChatHTML(c))
} else {
return c.SendString(generateWelcomeChatHTML())
}
}
func LoadChatInputHandler(c *fiber.Ctx) error {
if c.Cookies("jade-edgedb-auth-token") == "" {
out, err := chatInputTmpl.Execute(pongo2.Context{"IsLogin": false, "HaveKey": false, "IsSubscribed": false, "IsLimiteReached": false})
if err != nil {
fmt.Println("Error executing chat input template")
panic(err)
}
return c.SendString(out)
} else {
isPremium, isBasic := IsCurrentUserSubscribed(c)
out, err := chatInputTmpl.Execute(pongo2.Context{"IsLogin": checkIfLogin(c), "HaveKey": checkIfHaveKey(c), "IsSubscribed": isPremium, "IsLimiteReached": IsCurrentUserLimiteReached(c, isBasic)})
if err != nil {
fmt.Println("Error executing chat input template")
panic(err)
}
return c.SendString(out)
}
}
type TemplateMessage struct {
Icon string
Content string
Hidden bool
Id string
Name string
LLMID string
Model string
ModelID string
}
func generateChatHTML(c *fiber.Ctx) string {
// Println the name of the current conversation
var currentConv Conversation
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
SELECT global currentConversation { name }`, &currentConv)
if err != nil {
fmt.Println("Error getting current conversation")
panic(err)
}
// 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 = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("Error getting messages")
panic(err)
}
var user User
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
SELECT global currentUser { id } LIMIT 1
`, &user)
if err != nil {
fmt.Println("Error getting user")
panic(err)
}
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
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.Println("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.Println("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 := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
SELECT Message {
content
}
FILTER
.id = <uuid>$0;
`, &selectedMessage, messageUUID)
if err != nil {
fmt.Println("Error getting message")
panic(err)
}
out, err := userTmpl.Execute(pongo2.Context{"Content": markdownToHTML(selectedMessage.Content), "ID": id})
if err != nil {
fmt.Println("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
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 {
messageUUID, _ := edgedb.ParseUUID(messageId)
var selectedMessage Message
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}).QuerySingle(edgeCtx, `
SELECT Message {
content,
area: {
id,
position,
},
llm : {
name,
modelInfo : {
modelID,
name,
}
}
}
FILTER
.id = <uuid>$0;
`, &selectedMessage, messageUUID)
if err != nil {
panic(err)
}
if onlyContent == "true" {
return markdownToHTML(selectedMessage.Content)
}
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>"
out += "</div>"
out += "<div class='message-body'>"
out += " <ct class='content'>"
out += markdownToHTML(selectedMessage.Content)
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": authCookie}).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)
}
_ = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}).Execute(edgeCtx, `
UPDATE Message
FILTER .id = <uuid>$0
SET {selected := true};
`, messageUUID)
return out
}
func generateWelcomeChatHTML() string {
welcomeMessage, err := welcomeChatTmpl.Execute(pongo2.Context{})
if err != nil {
fmt.Println("Error executing welcome chat template")
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'>"
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, "DontShowName": true})
if err != nil {
fmt.Println("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 generateHelpChatHandler(c *fiber.Ctx) error {
return c.SendString(generateHelpChatHTML())
}
func generateHelpChatHTML() string {
out, err := explainLLMconvChatTmpl.Execute(pongo2.Context{})
if err != nil {
fmt.Println("Error executing explain LLM and Conversation template")
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'>"
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>`
NextMessages := []TemplateMessage{}
nextMsg := TemplateMessage{
Icon: "icons/bouvai2.png", // Assuming Icon is a field you want to include from Message
Content: out + 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, "DontShowName": true})
if err != nil {
fmt.Println("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 := `
<h1>API keys</h1>
<p class="mt-2">JADE require at least one API key to work. Add one in the settings at the bottom right of the page.</p>
<p>API keys are unique codes that allow you to access and use different Large Language Model (LLM) providers.</p>
<p>Most providers offer free credits when you first sign up or per minute, which means you have a balance in your account that you can use to generate messages.</p>
<p><strong>You pay for the service based on the number of tokens input and generated.</strong> A token is a small unit of text, roughly equivalent to 3 characters. So, a small input and output text will cost very little.</p>
<h2>Start for free</h2>
<p>Groq and Google offer free messages per minutes, enough for a conversation. Allowing you to use their models with JADE for free.</p>
<p>OpenAI and Anthropic offer 5$ of free credits when creating an account. So you can try JADE with their models for free.</p>
<p>As an example, I have been using the app for 5 months, sending around 1000 messages/month and it cost me 2-3$/month. While using GPT4, Claude 3.5, Codestral, Gemini 1.5 pro, Llama 70b, ect.</p>
<h2>Get a key</h2>
<p>To get a key and learn more about the different LLM providers and their offerings, check out their websites:</p>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://platform.openai.com/account/api-keys"
target="_blank"
>
Get OpenAI API key
</a>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://console.anthropic.com/"
target="_blank"
>
Get Anthropic API key
</a>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://console.mistral.ai/"
target="_blank"
>
Get Mistral API key
</a>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://console.groq.com/"
target="_blank"
>
Get Groq API key
</a>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://aistudio.google.com/app/apikey"
target="_blank"
>
Get Google API key
</a>
<a class="button is-small is-primary is-outlined mt-1"
href="https://build.nvidia.com/explore/discover"
target="_blank">Get Nvidia NIM API key</a>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://docs.perplexity.ai/docs/getting-started"
target="_blank"
>
Get Perplexity API key
</a>
<a
class="button is-small is-primary is-outlined mt-1"
href="https://fireworks.ai/login"
target="_blank"
>
Get Fireworks API key
</a>
<a class="button is-small is-primary is-outlined mt-1"
href="https://www.together.ai/"
target="_blank">Get Together AI API key</a>
<a class="button is-small is-primary is-outlined mt-1"
href="https://platform.deepseek.com/"
target="_blank">Get DeepSeek API key</a>
<br/>
<p>Note: Key are encrypted and saved on a secure database link only with the app.</p>`
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",
Content: welcomeMessage,
Hidden: false,
Id: "0",
Name: "JADE",
}
NextMessages = append(NextMessages, nextMsg)
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": true, "DontShowName": true})
if err != nil {
fmt.Println("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 := `
<h1 class="mt-2">Terms of Service</h1>
<h2>1. Acceptance of Terms</h2>
<p>By using JADE (the "App"), you agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, please do not use the App.</p>
<h2>2. Description of Service</h2>
<p>The App is a chatbot that makes requests to various third-party APIs to provide information and services. The App is provided "as is" and "as available" without any warranties of any kind.</p>
<h2>3. User Responsibilities</h2>
<p>- You are responsible for any content you generate or share using the App.</p>
<p>- You agree not to use the App for any unlawful or harmful activities.</p>
<h2>4. Authentication and Payment</h2>
<p>- Authentication for the App is managed by Google and GitHub. We are not responsible for any issues related to authentication, including but not limited to, unauthorized access or data breaches. Please refer to Google and GitHub's respective terms of service and privacy policies for more information.</p>
<p>- Payments for any services within the App are processed by Stripe. We are not responsible for any issues related to payment processing, including but not limited to, transaction errors or unauthorized transactions. Please refer to Stripe's terms of service and privacy policy for more information.</p>
<h2>5. Disclaimer of Warranties</h2>
<p>- The App is provided without warranties of any kind, either express or implied, including but not limited to, implied warranties of merchantability, fitness for a particular purpose, or non-infringement.</p>
<p>- We do not guarantee the accuracy, completeness, or usefulness of any information provided by the App.</p>
<h2>6. Limitation of Liability</h2>
<p>- In no event shall BouvAI, its affiliates, or its licensors be liable for any indirect, incidental, special, consequential, or punitive damages, including but not limited to, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your use or inability to use the App; (ii) any unauthorized access to or use of our servers and/or any personal information stored therein; (iii) any bugs, viruses, trojan horses, or the like that may be transmitted to or through our App by any third party; or (iv) any errors or omissions in any content or for any loss or damage incurred as a result of the use of any content posted, emailed, transmitted, or otherwise made available through the App, whether based on warranty, contract, tort (including negligence), or any other legal theory, whether or not we have been informed of the possibility of such damage.</p>
<h2>7. Data Privacy</h2>
<p>- We are not responsible for any data leaks or breaches that may occur. Users are advised to use the App at their own risk.</p>
<p>- Stored data include messages, conversations, usage, bots and keys. I do not strore any personal infos, google and github send me your email, name and avatar as minimum at your first login but I do not save them.</p>
<h2>8. Changes to the Terms</h2>
<p>We reserve the right to modify these Terms at any time. Any changes will be effective immediately upon posting the updated Terms on our website or within the App. Your continued use of the App after any such changes constitutes your acceptance of the new Terms.</p>
<h2>9. Governing Law</h2>
<p>These Terms shall be governed and construed in accordance with the laws of Luxembourg, without regard to its conflict of law principles.</p>
<h2>10. Contact Information</h2>
<p>If you have any questions about these Terms, please contact us at adrien.bouvais@bouvai.com.</p>
<strong>BouvAI</strong>
`
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, "DontShowName": true})
if err != nil {
fmt.Println("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(c *fiber.Ctx) 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 := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, "SELECT global currentUser { stripe_id, email } LIMIT 1;", &result)
if err != nil {
fmt.Println("Error getting current user")
panic(err)
}
clientSecretSession := CreateClientSecretSession(c)
stripeTable := `
<stripe-pricing-table
pricing-table-id="prctbl_1PJAxDP2nW0okNQyY0Q3mbg4"
publishable-key="pk_live_51OxXuWP2nW0okNQyme1qdwbL535jbMmM1uIUi6U5zcvEUUwKraktmpCzudXNdPSTxlHpw2FbCtxpwbyFFcasQ7aj000tJJGpWW"
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>" + 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.Println("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
}
// Button actions
func DeleteMessageHandler(c *fiber.Ctx) error {
messageId := c.FormValue("id")
messageUUID, err := edgedb.ParseUUID(messageId)
if err != nil {
fmt.Println("Error parsing UUID")
panic(err)
}
// Delete all messages
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("Error deleting messages")
panic(err)
}
return c.SendString(generateChatHTML(c))
}
func GetEditMessageFormHandler(c *fiber.Ctx) error {
id := c.FormValue("id")
idUUID, _ := edgedb.ParseUUID(id)
var message Message
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(context.Background(), `
SELECT Message { content }
FILTER .id = <uuid>$0;
`, &message, idUUID)
if err != nil {
fmt.Println("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
}
out, err := messageEditTmpl.Execute(pongo2.Context{"Content": message.Content, "ID": id, "Rows": rows})
if err != nil {
fmt.Println("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.Println("Error unmarshalling selected LLM IDs")
panic(err)
}
var message Message
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(context.Background(), `
SELECT Message { content }
FILTER .id = <uuid>$0;
`, &message, messageUUID)
if err != nil {
fmt.Println("Error getting message")
panic(err)
}
// Delete messages
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("Error deleting messages")
panic(err)
}
return c.SendString(GeneratePlaceholderHTML(c, 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.Println("Error unmarshalling selected LLM IDs")
panic(err)
}
if len(selectedLLMIds) == 0 {
return c.SendString("")
}
// Delete messages
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("Error deleting messages")
panic(err)
}
return c.SendString(GeneratePlaceholderHTML(c, message, selectedLLMIds, true))
}
func ClearChatHandler(c *fiber.Ctx) error {
// Delete the default conversation
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
DELETE Area
FILTER .conversation = global currentConversation;
`)
if err != nil {
fmt.Println("Error deleting messages")
panic(err)
}
return c.SendString(generateChatHTML(c))
}
// Popover stuff
func LoadUsageKPIHandler(c *fiber.Ctx) error {
if !checkIfLogin(c) || !checkIfHaveKey(c) {
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.Println("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 = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("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 := usagePopoverTmpl.Execute(pongo2.Context{
"usages": usages,
"TotalCost": TotalCost,
"TotalCount": TotalCount,
"Date": BeautifullDate,
"DateID": InputDate.Format("01-2006"),
"IsActive": IsActive,
})
if err != nil {
fmt.Println("Error generating usage")
panic(err)
}
return c.SendString(out)
}
func GenerateModelPopoverHTML(refresh bool, c *fiber.Ctx) string {
var llms []LLM
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("Error loading LLMs")
panic(err)
}
var modelInfos []ModelInfo
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Query(edgeCtx, `
SELECT ModelInfo {
modelID,
name,
company : {
name,
icon
}
}
FILTER .modelID != 'none' AND .company.name != 'huggingface' AND .company IN global currentUser.setting.keys.company
ORDER BY .company.name ASC THEN .name ASC
`, &modelInfos)
if err != nil {
fmt.Println("Error getting models")
panic(err)
}
isPremium, _ := IsCurrentUserSubscribed(c)
out, err := modelPopoverTmpl.Execute(pongo2.Context{
"IsLogin": checkIfLogin(c),
"LLMs": llms,
"ModelInfos": modelInfos,
"DeleteUpdate": refresh,
"IsPremium": isPremium,
})
if err != nil {
fmt.Println("Error generating model popover")
panic(err)
}
return out
}
func LoadModelSelectionHandler(c *fiber.Ctx) error {
if !checkIfLogin(c) || !checkIfHaveKey(c) {
return c.SendString("")
}
return c.SendString(GenerateModelPopoverHTML(false, c))
}
func GenerateConversationPopoverHTML(isActive bool, c *fiber.Ctx) string {
var conversations []Conversation
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Query(edgeCtx, `
SELECT Conversation {
name,
position,
selected,
id
}
FILTER .user = global currentUser
ORDER BY .position
`, &conversations)
if err != nil {
fmt.Println("Error loading conversations")
panic(err)
}
selectedIsDefault := false
for _, conversation := range conversations {
if conversation.Name == "Default" && conversation.Selected {
selectedIsDefault = true
}
}
out, err := conversationPopoverTmpl.Execute(pongo2.Context{
"Conversations": conversations,
"IsActive": isActive,
"SelectedIsDefault": selectedIsDefault,
})
if err != nil {
fmt.Println("Error generating conversation popover")
panic(err)
}
return out
}
func LoadConversationSelectionHandler(c *fiber.Ctx) error {
if !checkIfLogin(c) || !checkIfHaveKey(c) {
return c.SendString("")
}
return c.SendString(GenerateConversationPopoverHTML(false, c))
}
func RefreshConversationSelectionHandler(c *fiber.Ctx) error {
IsActive := c.FormValue("IsActive") == "true"
return c.SendString(GenerateConversationPopoverHTML(!IsActive, c))
}
func LoadSettingsHandler(c *fiber.Ctx) error {
if !checkIfLogin(c) {
return c.SendString("")
}
var user User
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, `
SELECT User {
email
}
FILTER .id = global currentUser.id
`, &user)
if err != nil {
fmt.Println("Error loading user")
panic(err)
}
// Percent encoding of the email
user.Email = url.QueryEscape(user.Email)
stripeSubLink := "https://billing.stripe.com/p/login/6oE6sc0PTfvq1Hi288?prefilled_email=" + user.Email
openaiExists, anthropicExists, mistralExists, groqExists, googleExists, perplexityExists, fireworksExists, nimExists, togetherExists, deepseekExists := getExistingKeysNew(c)
isPremium, isBasic := IsCurrentUserSubscribed(c)
out, err := settingPopoverTmpl.Execute(pongo2.Context{
"IsLogin": checkIfLogin(c),
"OpenaiExists": openaiExists,
"AnthropicExists": anthropicExists,
"MistralExists": mistralExists,
"GroqExists": groqExists,
"GoogleExists": googleExists,
"NimExists": nimExists,
"PerplexityExists": perplexityExists,
"FireworksExists": fireworksExists,
"TogetherExists": togetherExists,
"DeepseekExists": deepseekExists,
"AnyExists": fireworksExists || openaiExists || anthropicExists || mistralExists || groqExists || googleExists || perplexityExists || nimExists || togetherExists || deepseekExists,
"isPremium": isPremium,
"isBasic": isBasic,
"StripeSubLink": stripeSubLink,
})
if err != nil {
fmt.Println("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 := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).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.Println("Error creating conversation")
panic(err)
}
return c.SendString(GenerateConversationPopoverHTML(true, c))
}
func DeleteConversationHandler(c *fiber.Ctx) error {
conversationId := c.FormValue("conversationId")
conversationUUID, err := edgedb.ParseUUID(conversationId)
if err != nil {
fmt.Println("Error parsing UUID")
panic(err)
}
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
DELETE Conversation
FILTER .user = global currentUser AND .id = <uuid>$0;
`, conversationUUID)
if err != nil {
fmt.Println("Error deleting conversation")
panic(err)
}
// Select the default conversation
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
UPDATE Conversation
FILTER .user = global currentUser AND .name = 'Default'
SET {
selected := true
};
`)
if err != nil {
fmt.Println("Error selecting default conversation")
panic(err)
}
reloadChatTriggerHTML := `
<hx hx-get="/loadChat" hx-trigger="load once" hx-swap="outerHTML" hx-target="#chat-container" style="display: none;"></hx>
`
return c.SendString(GenerateConversationPopoverHTML(true, c) + reloadChatTriggerHTML)
}
func SelectConversationHandler(c *fiber.Ctx) error {
conversationId := c.FormValue("conversation-id")
conversationUUID, err := edgedb.ParseUUID(conversationId)
if err != nil {
fmt.Println("Error parsing UUID")
panic(err)
}
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
UPDATE Conversation
FILTER .user = global currentUser
SET {
selected := false
};
`, conversationUUID)
if err != nil {
fmt.Println("Error unselecting conversations")
panic(err)
}
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
UPDATE Conversation
FILTER .user = global currentUser AND .id = <uuid>$0
SET {
selected := true
};
`, conversationUUID)
if err != nil {
fmt.Println("Error selecting conversations")
panic(err)
}
return c.SendString(generateChatHTML(c))
}
func ArchiveDefaultConversationHandler(c *fiber.Ctx) error {
name := c.FormValue("conversation-name-input")
err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
UPDATE Conversation
FILTER .user = global currentUser AND .name = 'Default'
SET {
name := <str>$0
};
`, name)
if err != nil {
fmt.Println("Error archiving default conversation")
panic(err)
}
err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).Execute(edgeCtx, `
WITH
C := (
SELECT Conversation
FILTER .user = global currentUser
)
INSERT Conversation {
name := "Default",
user := global currentUser,
position := count(C) + 1
}
`, name)
if err != nil {
fmt.Println("Error creating conversation")
panic(err)
}
return c.SendString(GenerateConversationPopoverHTML(true, c))
}