diff --git a/Chat.go b/Chat.go index 01810cc..32c27ec 100644 --- a/Chat.go +++ b/Chat.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "fmt" "net/url" "sort" "strings" @@ -15,6 +16,7 @@ import ( 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}) @@ -64,7 +66,7 @@ func LoadChatHandler(c *fiber.Ctx) error { if checkIfLogin() { if IsCurrentUserLimiteReached() && !IsCurrentUserSubscribed() { return c.SendString(generateLimitReachedChatHTML()) - } else if getCurrentUserKeys() == nil { + } else if !checkIfHaveKey() { return c.SendString(generateEnterKeyChatHTML()) } return c.SendString(generateChatHTML()) @@ -84,9 +86,33 @@ type TemplateMessage struct { } func generateChatHTML() string { - // Get the messages from the database // Maybe redo that to be area by area because look like shit rn. It come from early stage of dev. It work tho soooo... - Messages := getAllMessages() + 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 { + panic(err) + } htmlString := "
" @@ -594,12 +620,13 @@ func LoadModelSelectionHandler(c *fiber.Ctx) error { return c.SendString(GenerateModelPopoverHTML(false)) } -func GenerateConversationPopoverHTML(refresh bool) string { +func GenerateConversationPopoverHTML(isActive bool) string { var conversations []Conversation err := edgeClient.Query(edgeCtx, ` SELECT Conversation { name, - position + position, + id } FILTER .user = global currentUser ORDER BY .position @@ -610,7 +637,7 @@ func GenerateConversationPopoverHTML(refresh bool) string { out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-conversation.html")).Execute(pongo2.Context{ "Conversations": conversations, - "IsActive": refresh, + "IsActive": isActive, }) if err != nil { panic(err) @@ -627,7 +654,8 @@ func LoadConversationSelectionHandler(c *fiber.Ctx) error { } func RefreshConversationSelectionHandler(c *fiber.Ctx) error { - return c.SendString(GenerateConversationPopoverHTML(true)) + IsActive := c.FormValue("IsActive") == "true" + return c.SendString(GenerateConversationPopoverHTML(!IsActive)) } func LoadSettingsHandler(c *fiber.Ctx) error { @@ -670,3 +698,58 @@ func LoadSettingsHandler(c *fiber.Ctx) error { } return c.SendString(out) } + +func CreateConversationHandler(c *fiber.Ctx) error { + if !checkIfLogin() || !checkIfHaveKey() { + return c.SendString("") + } + + 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 := $0, + user := global currentUser, + position := count(C) + 1 + } + `, name) + if err != nil { + panic(err) + } + + return c.SendString(GenerateConversationPopoverHTML(true)) +} + +func SelectConversationHandler(c *fiber.Ctx) error { + if !checkIfLogin() || !checkIfHaveKey() { + return c.SendString("") + } + + conversationId := c.FormValue("conversation-id") + conversationUUID, err := edgedb.ParseUUID(conversationId) + if err != nil { + // Handle the error + panic(err) + } + + err = edgeClient.Execute(edgeCtx, ` + SET global currentConversation := ( + SELECT Conversation + FILTER .id = $0 + LIMIT 1 + ); + `, conversationUUID) + if err != nil { + panic(err) + } + + return c.SendString(generateChatHTML()) +} diff --git a/LLM.go b/LLM.go index 3a458c1..b6eb990 100644 --- a/LLM.go +++ b/LLM.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "strconv" "github.com/edgedb/edgedb-go" @@ -139,3 +140,31 @@ func updateLLMPositionBatch(c *fiber.Ctx) error { return nil } + +func updateConversationPositionBatch(c *fiber.Ctx) error { + var positionUpdates []PositionUpdate + if err := c.BodyParser(&positionUpdates); err != nil { + return err + } + + for _, update := range positionUpdates { + fmt.Println(update.ID) + idUUID, err := edgedb.ParseUUID(update.ID) + if err != nil { + panic(err) + } + + err = edgeClient.Execute(edgeCtx, ` + UPDATE Conversation + FILTER .id = $0 AND .user = global currentUser + SET { + position := $1 + }; + `, idUUID, int32(update.Position)) + if err != nil { + panic(err) + } + } + + return nil +} diff --git a/Request.go b/Request.go index cb684c7..b38d7f5 100644 --- a/Request.go +++ b/Request.go @@ -1,3 +1,13 @@ +// I guess it should be some kind of package with different part for different Company +// I will maybe do it in the future, rn, I don't have time to learn that and I like it like that +// It do need more time and try and error to do all of them but they are fully independant +// And this is simple, I don't need to trick my mind to undersant that some part are share, ect +// If I want to see how I go from the message to the response, I can. That what I want + +// If you are wondering how it work: +// User send message -> Generate HTML of one user message and one bot placeholder ---- +// -> Send HTML and append it to the chat container -> The placeholder do a load HTMX request to GenerateMultipleMessagesHandler ---- +// -> Make multiple request in parallel to all APIs -> Send one SSE event per message receive. package main import ( @@ -230,3 +240,20 @@ func GenerateMultipleMessagesHandler(c *fiber.Ctx) error { return c.SendString("") } + +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 { + panic(err) + } +} diff --git a/RequestGoogle.go b/RequestGoogle.go index 947150b..e01bfb7 100644 --- a/RequestGoogle.go +++ b/RequestGoogle.go @@ -1,3 +1,5 @@ +// Yes Google do not work because I am in Europe and I can't get an API key... +// I can't dev, I will try using a VPN I guess at least so the features is available for people outside of Europe package main import ( diff --git a/RequestGooseai.go b/RequestGooseai.go index 076d3d2..92c6d7a 100644 --- a/RequestGooseai.go +++ b/RequestGooseai.go @@ -1,3 +1,6 @@ +// It work but I disable it because it is not chat API +// It is text completion, not chat completion. But they will soon release API for chat +// So I leave it here for now package main import ( diff --git a/Stripe.go b/Stripe.go index c25783a..c1e57d8 100644 --- a/Stripe.go +++ b/Stripe.go @@ -1,3 +1,4 @@ +// That work, you can pay. Fully functional package main import ( diff --git a/database.go b/database.go index ca6405f..aab501a 100644 --- a/database.go +++ b/database.go @@ -1,15 +1,21 @@ +// Here are all type that I use with EdgeDB. I really love how this works together. +// The functions are from the very beginning. Of the app, I will remove it I think. +// I need to think of a better way to organize that. package main import ( "context" + "fmt" "time" "github.com/edgedb/edgedb-go" ) +// So I have one client and one context for the database. All query use the same and it work well so far. var edgeCtx context.Context var edgeClient *edgedb.Client +// I will not put a comment on all type, I think they are self-explaining. type Identity struct { ID edgedb.UUID `edgedb:"id"` Issuer string `edgedb:"issuer"` @@ -24,7 +30,7 @@ type User struct { Avatar string `edgedb:"avatar"` } -type Key struct { +type Key struct { // API key ID edgedb.UUID `edgedb:"id"` Name string `edgedb:"name"` Company CompanyInfo `edgedb:"company"` @@ -32,7 +38,7 @@ type Key struct { Date time.Time `edgedb:"date"` } -type Setting struct { +type Setting struct { // Per user ID edgedb.UUID `edgedb:"id"` Keys []Key `edgedb:"keys"` DefaultModel edgedb.OptionalStr `edgedb:"default_model"` @@ -46,6 +52,10 @@ type Conversation struct { User User `edgedb:"user"` } +// An area is in between messages and conversation. +// In a normal chat, you have a list of message, easy. By here you need to add, kind of a new dimension. +// All "message" can have multiple messages. So I created a new type named Area. +// A conversation is a list of Area and an Area is a list of Message. Easy enough. type Area struct { ID edgedb.UUID `edgedb:"id"` Position int64 `edgedb:"position"` @@ -56,7 +66,7 @@ type Message struct { ID edgedb.UUID `edgedb:"id"` Content string `edgedb:"content"` Role string `edgedb:"role"` - Selected bool `edgedb:"selected"` + Selected bool `edgedb:"selected"` // Selected can also be seen as "Active". This is the message that will be use for the request. Date time.Time `edgedb:"date"` Area Area `edgedb:"area"` Conv Conversation `edgedb:"conversation"` @@ -73,6 +83,9 @@ type Usage struct { OutputToken int32 `edgedb:"output_token"` } +// A LLM is a bad name but I like it. +// It is more one instance of a model with it parameters. +// Maybe I will add more options later. type LLM struct { ID edgedb.UUID `edgedb:"id"` Name string `edgedb:"name"` @@ -116,99 +129,34 @@ func init() { edgeClient = client } -func getLastArea() edgedb.UUID { - var inserted struct{ id edgedb.UUID } - err := edgeClient.QuerySingle(edgeCtx, ` - select Area - filter .conversation.name = 'Default' AND .conversation.user = global currentUser - order by .position desc - limit 1 - `, &inserted) - if err != nil { - panic(err) - } - return inserted.id -} - func checkIfLogin() bool { var result User err := edgeClient.QuerySingle(edgeCtx, "SELECT global currentUser LIMIT 1;", &result) 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 { - panic(err) - } -} - -func insertNewConversation() edgedb.UUID { - var inserted struct{ id edgedb.UUID } - err := edgeClient.QuerySingle(edgeCtx, ` - WITH - C := ( - SELECT Conversation - FILTER .user = global currentUser - ) - INSERT Conversation { - name := 'Default', - user := global currentUser, - position := count(C) + 1 - } - `, &inserted) - if err != nil { - panic(err) - } - return inserted.id -} - func insertArea() (edgedb.UUID, int64) { - // If the Default conversation doesn't exist, create it. - var convExists bool - edgeClient.QuerySingle(edgeCtx, ` - select exists ( - select Conversation - filter .name = 'Default' AND .user = global currentUser - ); - `, &convExists) - if !convExists { - insertNewConversation() - } // Insert a new area. var inserted struct{ id edgedb.UUID } err := edgeClient.QuerySingle(edgeCtx, ` WITH - positionVar := count((SELECT Area FILTER .conversation.name = 'Default' AND .conversation.user = global currentUser)) + 1 + positionVar := count((SELECT Area FILTER .conversation = global currentConversation AND .conversation.user = global currentUser)) + 1 INSERT Area { position := positionVar, - conversation := ( - SELECT Conversation - FILTER .name = 'Default' AND .user = global currentUser - LIMIT 1 - ) + conversation := global currentConversation } `, &inserted) if err != nil { + fmt.Println("Couldn't insert area") panic(err) } var positionSet struct{ position int64 } err = edgeClient.QuerySingle(edgeCtx, ` SELECT Area { - position + position, } - FILTER .conversation.name = 'Default' AND .conversation.user = global currentUser + FILTER .conversation = global currentConversation AND .conversation.user = global currentUser ORDER BY .position desc LIMIT 1 `, &positionSet) @@ -221,9 +169,21 @@ func insertArea() (edgedb.UUID, int64) { func insertUserMessage(content string) edgedb.UUID { // Insert a new user. - lastAreaID := getLastArea() - var inserted struct{ id edgedb.UUID } + var lastAreaID edgedb.UUID err := edgeClient.QuerySingle(edgeCtx, ` + SELECT Area { + id + } + FILTER .conversation = global currentConversation AND .conversation.user = global currentUser + ORDER BY .position desc + LIMIT 1 + `, &lastAreaID) + if err != nil { + panic(err) + } + + var inserted struct{ id edgedb.UUID } + err = edgeClient.QuerySingle(edgeCtx, ` INSERT Message { role := $0, content := $1, @@ -231,11 +191,7 @@ func insertUserMessage(content string) edgedb.UUID { SELECT Area FILTER .id = $2 ), - conversation := ( - SELECT Conversation - FILTER .name = 'Default' AND .user = global currentUser - LIMIT 1 - ), + conversation := global currentConversation, selected := true, llm := ( SELECT LLM FILTER .id = "a32c43ec-12fc-11ef-9dc9-b38e0de8bff0" @@ -249,9 +205,21 @@ func insertUserMessage(content string) edgedb.UUID { } func insertBotMessage(content string, selected bool, llmUUID edgedb.UUID) edgedb.UUID { - lastAreaID := getLastArea() - var inserted struct{ id edgedb.UUID } + var lastAreaID edgedb.UUID err := edgeClient.QuerySingle(edgeCtx, ` + SELECT Area { + id + } + FILTER .conversation = global currentConversation AND .conversation.user = global currentUser + ORDER BY .position desc + LIMIT 1 + `, &lastAreaID) + if err != nil { + panic(err) + } + + var inserted struct{ id edgedb.UUID } + err = edgeClient.QuerySingle(edgeCtx, ` INSERT Message { role := $0, content := $2, @@ -279,42 +247,6 @@ func insertBotMessage(content string, selected bool, llmUUID edgedb.UUID) edgedb return inserted.id } -func getAllMessages() []Message { - // If no CurrentUser, return an empty array - if !checkIfLogin() { - return []Message{} - } - - var messages []Message - - err := edgeClient.Query(edgeCtx, ` - SELECT Message { - id, - selected, - role, - content, - date, - llm : { - name, - modelInfo : { - modelID, - name, - company : { - icon - } - } - } - } - FILTER .conversation.name = 'Default' AND .conversation.user = global currentUser - ORDER BY .date ASC - `, &messages) - if err != nil { - panic(err) - } - - return messages -} - func getAllSelectedMessages() []Message { // If no CurrentUser, return an empty array if !checkIfLogin() { @@ -338,7 +270,7 @@ func getAllSelectedMessages() []Message { } } } - } FILTER .conversation.name = 'Default' AND .conversation.user = global currentUser AND .selected = true + } FILTER .conversation = global currentConversation AND .conversation.user = global currentUser AND .selected = true ORDER BY .date ASC `, &messages) if err != nil { @@ -348,16 +280,11 @@ func getAllSelectedMessages() []Message { return messages } -func getCurrentUserKeys() []Key { - var result []Key - err := edgeClient.Query(edgeCtx, "SELECT global currentUser.setting.keys", &result) +func checkIfHaveKey() bool { + var keys []Key + err := edgeClient.Query(edgeCtx, "SELECT global currentUser.setting.keys", &keys) if err != nil { panic(err) } - return result -} - -func checkIfHaveKey() bool { - keys := getCurrentUserKeys() return len(keys) > 0 } diff --git a/dbschema/default.esdl b/dbschema/default.esdl index de408ce..e3f9fba 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -8,6 +8,13 @@ module default { )) ); + global currentConversation := ( + assert_single(( + select Conversation + filter .name = 'Default' AND .user = global currentUser + )) + ); + type User { required setting: Setting; required stripe_id: str; diff --git a/dbschema/migrations/00043-m1wantz.edgeql b/dbschema/migrations/00043-m1wantz.edgeql new file mode 100644 index 0000000..96d9fe0 --- /dev/null +++ b/dbschema/migrations/00043-m1wantz.edgeql @@ -0,0 +1,9 @@ +CREATE MIGRATION m1wantzc6ebph75s6s5oqd4yfcjul575mqmaas5g3ch77f3ojuwluq + ONTO m1ospnjzsatmkntvczm5eu65omytyezeg3lanxeogtdqz2372t6cuq +{ + CREATE GLOBAL default::currentConversation := (std::assert_single((SELECT + default::User + FILTER + (.name = 'default') + ))); +}; diff --git a/dbschema/migrations/00044-m1xiilc.edgeql b/dbschema/migrations/00044-m1xiilc.edgeql new file mode 100644 index 0000000..e5ddf7a --- /dev/null +++ b/dbschema/migrations/00044-m1xiilc.edgeql @@ -0,0 +1,9 @@ +CREATE MIGRATION m1xiilci36z6h4kris43l3gb6w63y7lbql3ismz7coxobr6r2yhz6a + ONTO m1wantzc6ebph75s6s5oqd4yfcjul575mqmaas5g3ch77f3ojuwluq +{ + ALTER GLOBAL default::currentConversation USING (std::assert_single((SELECT + default::Conversation + FILTER + ((.name = 'Default') AND (.user = GLOBAL default::currentUser)) + ))); +}; diff --git a/login.go b/login.go index f1ab1b5..0b98306 100644 --- a/login.go +++ b/login.go @@ -236,6 +236,16 @@ func handleCallbackSignup(c *fiber.Ctx) error { edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": tokenResponse.AuthToken}) + err = edgeClient.Execute(edgeCtx, ` + INSERT Conversation { + name := 'Default', + user := global currentUser, + position := 1 + }`) + if err != nil { + panic(err) + } + return c.Redirect("/", fiber.StatusPermanentRedirect) } diff --git a/main.go b/main.go index f8dfedf..09f2061 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,31 @@ +// Welcome welcome, I guess you started here +// Thank you for reviewing my code. + +// As for context. This is the absolutely first time I use Golang or EdgeDB. +// So far I only did python. I have a master but in thermal engineering, not CS. + +// My philosophy in coding is simplicity. A "clean code" is a simple code. +// For me you should need few comments. Variables, type and functions name should be enough to understand. +// And what's work, work. + +// To build my app I took a simple approach. +// All data are stored in database. If I need it, I ask the DB. +// There is no state in the server nor in the client (or very little for the client). + +// For example if I want to ask a question. Here a simplify version: +// 1. Add the user message to the DB. +// 2. Read all messages from the DB to send a request. +// 3. Add the bot message to the DB. +// 4. Read all messages from the DB to generate the chat HTML. +// So all query are simple query. That is my goal. In the future I may optimize it, but for dev, that is the best approach I think. + +// Also if I want to change the order of a list (you will see later). +// I first send the new positions -> update the db -> read the db to generate the HTML. + +// So Edgedb is the heart of the app. Everything pass by it. + +// It need A LOT of optimization, I first do something that work then optimize it. +// I hope you will like it. package main import ( @@ -51,7 +79,7 @@ func main() { // Import HTML using django engine/template engine := django.New("./views", ".html") - // Create new Fiber instance. Can use any framework. I use fiber for speed and simplicity + // Create new Fiber instance app := fiber.New(fiber.Config{ Views: engine, AppName: "JADE 2.0", @@ -92,6 +120,9 @@ func main() { app.Get("/loadKeys", LoadKeysHandler) app.Get("/loadSettings", LoadSettingsHandler) app.Post("/updateLLMPositionBatch", updateLLMPositionBatch) + app.Get("/createConversation", CreateConversationHandler) + app.Get("/selectConversation", SelectConversationHandler) + app.Post("/updateConversationPositionBatch", updateConversationPositionBatch) // Authentication app.Get("/signin", handleUiSignIn) @@ -145,6 +176,11 @@ func main() { log.Fatal(app.Listen(":8080")) } +// The route to add keys, idk where to put it so it's here +// Deserve a good REDO lol +// I mean, look at this shit... +// 300 lines, I'm sure it can be done in 50 +// TODO REDO and maybe find it a better place func addKeys(c *fiber.Ctx) error { openaiKey := c.FormValue("openai_key") anthropicKey := c.FormValue("anthropic_key") diff --git a/static/model-selection-menu.js b/static/model-selection-menu.js deleted file mode 100644 index c9fc759..0000000 --- a/static/model-selection-menu.js +++ /dev/null @@ -1,54 +0,0 @@ -let lastSelectedIndex = null; - -function toggleSelection(element) { - const elements = Array.from(document.getElementsByClassName('icon-text')); - const index = elements.indexOf(element); - - if (document.body.classList.contains('shift-pressed') && lastSelectedIndex !== null) { - const [start, end] = [lastSelectedIndex, index].sort((a, b) => a - b); - let allSelected = true; - for (let i = start; i <= end; i++) { - if (!elements[i].classList.contains('selected')) { - allSelected = false; - break; - } - } - for (let i = start; i <= end; i++) { - if (allSelected) { - elements[i].classList.remove('selected'); - elements[i].classList.add('unselected'); - } else { - elements[i].classList.add('selected'); - elements[i].classList.remove('unselected'); - } - } - lastSelectedIndex = null; - - const elements2 = Array.from(document.getElementsByClassName('icon-text')); - for (let i = 0; i < elements2.length; i++) { - elements2[i].classList.remove('shiftselected'); - } - } else if (document.body.classList.contains('shift-pressed') && lastSelectedIndex === null) { - lastSelectedIndex = index; - element.classList.toggle('shiftselected'); - } else { - element.classList.toggle('selected'); - element.classList.toggle('unselected'); - } - toggleSendButton(); -} - -function getSelectedModelsIDs() { - var selectedModelsIDs = []; - var selectedModels = document.getElementsByClassName('selected'); - for (var i = 0; i < selectedModels.length; i++) { - selectedModelsIDs.push(selectedModels[i].getAttribute('data-id')); - } - return selectedModelsIDs.length > 0 ? JSON.stringify(selectedModelsIDs) : '[]'; -} - -function toggleSendButton() { - var selectedModels = document.getElementsByClassName('selected'); - var sendButton = document.querySelector('button[disabled]'); - sendButton.disabled = selectedModels.length === 0; -} \ No newline at end of file diff --git a/utils.go b/utils.go index 52183d2..d9fab87 100644 --- a/utils.go +++ b/utils.go @@ -1,3 +1,6 @@ +// The usual utils files with some functions +// I do plan to change the markdownToHTML and addCopyButtonsToCode +// I will take example on openai and gemini and put a header on top of a code part with the button instead of inside package main import ( diff --git a/views/layouts/main.html b/views/layouts/main.html index be81c7a..b72272a 100644 --- a/views/layouts/main.html +++ b/views/layouts/main.html @@ -15,7 +15,6 @@ - diff --git a/views/partials/popover-conversation.html b/views/partials/popover-conversation.html index 65b376a..ef90678 100644 --- a/views/partials/popover-conversation.html +++ b/views/partials/popover-conversation.html @@ -1,7 +1,8 @@