diff --git a/Chat.go b/Chat.go index 0ec65bd..fa29f1f 100644 --- a/Chat.go +++ b/Chat.go @@ -18,14 +18,14 @@ func ChatPageHandler(c *fiber.Ctx) error { edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie}) } - fmt.Println("Current User: ", getCurrentUser()) + fmt.Println("Current User: ", getCurrentUser(), " - ", checkIfLogin()) - return c.Render("chat", fiber.Map{"IsLogin": checkIfLogin()}, "layouts/main") + return c.Render("chat", fiber.Map{"IsLogin": checkIfLogin(), "HaveKey": checkIfHaveKey()}, "layouts/main") } func LoadModelSelectionHandler(c *fiber.Ctx) error { CheckedModels := []string{"gpt-3.5-turbo"} // Default model - out, err := pongo2.Must(pongo2.FromFile("views/partials/modelsPopover.html")).Execute(pongo2.Context{ + out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-models.html")).Execute(pongo2.Context{ "CompanyInfos": CompanyInfos, "CheckedModels": CheckedModels, }) @@ -53,7 +53,7 @@ func LoadUsageKPIHandler(c *fiber.Ctx) error { log.Fatal(err) } - out, err := pongo2.Must(pongo2.FromFile("views/partials/usagePopover.html")).Execute(pongo2.Context{ + out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-usage.html")).Execute(pongo2.Context{ "TotalUsage": TotalUsage, }) if err != nil { @@ -64,6 +64,16 @@ func LoadUsageKPIHandler(c *fiber.Ctx) error { return c.SendString(out) } +func LoadSettingsHandler(c *fiber.Ctx) error { + out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-settings.html")).Execute(pongo2.Context{"IsLogin": checkIfLogin()}) + if err != nil { + c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error rendering template", + }) + } + return c.SendString(out) +} + func DeleteMessageHandler(c *fiber.Ctx) error { messageId := c.FormValue("id") @@ -92,6 +102,9 @@ func DeleteMessageHandler(c *fiber.Ctx) error { func LoadChatHandler(c *fiber.Ctx) error { if checkIfLogin() { + if getCurrentUserKeys() == nil { + return c.SendString(generateEnterKeyChatHTML()) + } return c.SendString(generateChatHTML()) } else { return c.SendString(generateWelcomeChatHTML()) @@ -210,19 +223,53 @@ func GetMessageContentHandler(c *fiber.Ctx) error { } func generateWelcomeChatHTML() string { + welcomeMessage := `To start using JADE, please login.` + + loginButton := ` + + Log in + ` + htmlString := "
" NextMessages := []NextMessage{} nextMsg := NextMessage{ Icon: "bouvai2", // Assuming Icon is a field you want to include from Message - Content: markdownToHTML("Hi, I'm Bouvai. How can I help you today?"), + Content: "
" + 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}) + botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": !true}) + if err != nil { + panic(err) + } + htmlString += botOut + htmlString += "
" + htmlString += "
" + + // Render the HTML template with the messages + return htmlString +} + +func generateEnterKeyChatHTML() string { + welcomeMessage := `To start using JADE, please enter at least one key in the settings.` + + htmlString := "
" + + NextMessages := []NextMessage{} + nextMsg := NextMessage{ + Icon: "bouvai2", // Assuming Icon is a field you want to include from Message + Content: "
" + markdownToHTML(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 { panic(err) } diff --git a/RequestAnthropic.go b/RequestAnthropic.go index 99c20d1..6b2e6df 100644 --- a/RequestAnthropic.go +++ b/RequestAnthropic.go @@ -8,6 +8,8 @@ import ( "net/http" "github.com/edgedb/edgedb-go" + "github.com/flosch/pongo2" + "github.com/gofiber/fiber/v2" ) type AnthropicChatCompletionRequest struct { @@ -124,8 +126,69 @@ func EdgeMessages2AnthropicMessages(messages []Message) []AnthropicMessage { return AnthropicMessages } +func TestAnthropicKey(apiKey string) bool { + url := "https://api.anthropic.com/v1/messages" + + AnthropicMessages := []AnthropicMessage{ + { + Role: "user", + Content: "Hello", + }, + } + + requestBody := AnthropicChatCompletionRequest{ + Model: "claude-3-haiku-20240307", + Messages: AnthropicMessages, + MaxTokens: 10, + Temperature: 0, + } + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return false + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + return false + } + + req.Header.Set("content-Type", "application/json") + req.Header.Set("anthropic-version", "2023-06-01") + req.Header.Set("x-api-key", apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false + } + + var chatCompletionResponse AnthropicChatCompletionResponse + err = json.Unmarshal(body, &chatCompletionResponse) + return err == nil +} + func RequestAnthropic(model string, messages []Message, maxTokens int, temperature float64) (AnthropicChatCompletionResponse, error) { - apiKey := "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" // TODO Use env variable + var apiKey string + err := edgeClient.QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + key + } filter .company = $0 + ) + select filtered_keys.key limit 1 + `, &apiKey, "anthropic") + if err != nil { + return AnthropicChatCompletionResponse{}, fmt.Errorf("error getting OpenAI API key: %w", err) + } + url := "https://api.anthropic.com/v1/messages" AnthropicMessages := EdgeMessages2AnthropicMessages(messages) @@ -181,3 +244,74 @@ func RequestAnthropic(model string, messages []Message, maxTokens int, temperatu return chatCompletionResponse, nil } + +func addAnthropicKey(c *fiber.Ctx) error { + key := c.FormValue("key") + + // Check if the key already exists + err := edgeClient.QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + key + } filter .key = $0 and .company = "anthropic" + ) + select filtered_keys.key limit 1 + `, &key, key) + if err == nil { + return c.SendString("") + } + + if !TestAnthropicKey(key) { + fmt.Println("Invalid Anthropic API Key") + + NextMessages := []NextMessage{} + nextMsg := NextMessage{ + Icon: "bouvai2", // Assuming Icon is a field you want to include from Message + Content: "
" + markdownToHTML("Invalid Anthropic API Key"), + 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}) + if err != nil { + panic(err) + } + return c.SendString(botOut) + } + + err = edgeClient.Execute(edgeCtx, ` + UPDATE global currentUser.setting + SET { + keys += ( + INSERT Key { + company := $0, + key := $1, + name := $2, + } + ) + }`, "anthropic", key, "Anthropic API Key") + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addOpenaiKey") + fmt.Println(err) + } + + NextMessages := []NextMessage{} + nextMsg := NextMessage{ + Icon: "bouvai2", // Assuming Icon is a field you want to include from Message + Content: "
" + markdownToHTML("Key added successfully!"), + 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}) + if err != nil { + panic(err) + } + + return c.SendString(botOut) +} diff --git a/RequestOpenai.go b/RequestOpenai.go index 197a255..eabf6a3 100644 --- a/RequestOpenai.go +++ b/RequestOpenai.go @@ -8,6 +8,8 @@ import ( "net/http" "github.com/edgedb/edgedb-go" + "github.com/flosch/pongo2" + "github.com/gofiber/fiber/v2" ) type OpenaiChatCompletionRequest struct { @@ -115,8 +117,76 @@ func EdgeMessages2OpenaiMessages(messages []Message) []OpenaiMessage { return openaiMessages } +func TestOpenaiKey(apiKey string) bool { + url := "https://api.openai.com/v1/chat/completions" + + // Convert messages to OpenAI format + openaiMessages := []OpenaiMessage{ + { + Role: "user", + Content: "Hello", + }, + } + + requestBody := OpenaiChatCompletionRequest{ + Model: "gpt-3.5-turbo", + Messages: openaiMessages, + Temperature: 0, + } + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return false + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + return false + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false + } + + var chatCompletionResponse OpenaiChatCompletionResponse + err = json.Unmarshal(body, &chatCompletionResponse) + if err != nil { + return false + } + if chatCompletionResponse.Choices == nil { + return false + } + return false +} + func RequestOpenai(model string, messages []Message, temperature float64) (OpenaiChatCompletionResponse, error) { - apiKey := "sk-proj-f7StCvXCtcmiOOayiVmgT3BlbkFJlVtAcOo3JcrnGq1cPa5o" // TODO Use env variable + var apiKey string + err := edgeClient.QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + key + } filter .company = $0 + ) + select filtered_keys.key limit 1 + `, &apiKey, "openai") + if err != nil { + return OpenaiChatCompletionResponse{}, fmt.Errorf("error getting OpenAI API key: %w", err) + } + + fmt.Println("OpenAI API key: ", apiKey) + url := "https://api.openai.com/v1/chat/completions" // Convert messages to OpenAI format @@ -171,3 +241,103 @@ func RequestOpenai(model string, messages []Message, temperature float64) (Opena return chatCompletionResponse, nil } + +func addOpenaiKey(c *fiber.Ctx) error { + key := c.FormValue("key") + + // Check if the key already exists + var keyUUID edgedb.UUID + err := edgeClient.QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + id + } filter .key = $0 and .company = "openai" + ) + select filtered_keys.key limit 1 + `, &keyUUID, key) + if err == nil { + fmt.Println("Error in edgedb.Query: in addOpenaiKey") + return c.SendString("") + } + + if !TestOpenaiKey(key) { + fmt.Println("Invalid OpenAI API Key") + NextMessages := []NextMessage{} + nextMsg := NextMessage{ + Icon: "bouvai2", // Assuming Icon is a field you want to include from Message + Content: "
" + markdownToHTML("Invalid OpenAI API Key"), + 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}) + if err != nil { + panic(err) + } + return c.SendString(botOut) + } + + // Check if the company key already exists + err = edgeClient.QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + id + } filter .company = "openai" + ) + select filtered_keys.key limit 1 + `, &keyUUID, key) + if err != nil { + fmt.Println("Company key already exists") + err = edgeClient.Execute(edgeCtx, ` + UPDATE Key filter .company = $0 AND .key = $1 + SET { + key := $1, + } + `, "openai", key) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addOpenaiKey") + fmt.Println(err) + } + + return c.SendString("") + } + + fmt.Println("OpenAI API key: ", key) + + err = edgeClient.Execute(edgeCtx, ` + UPDATE global currentUser.setting + SET { + keys += ( + INSERT Key { + company := $0, + key := $1, + name := $2, + } + ) + }`, "openai", key, "OpenAI API Key") + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addOpenaiKey") + fmt.Println(err) + } + + NextMessages := []NextMessage{} + nextMsg := NextMessage{ + Icon: "bouvai2", // Assuming Icon is a field you want to include from Message + Content: "
Key added successfully!", + 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}) + if err != nil { + panic(err) + } + + return c.SendString(botOut) +} diff --git a/database.go b/database.go index e3a31d6..a79ea3a 100644 --- a/database.go +++ b/database.go @@ -13,19 +13,16 @@ var edgeCtx context.Context var edgeClient *edgedb.Client type User struct { - ID edgedb.UUID `edgedb:"id"` - Email string `edgedb:"email"` - Name string `edgedb:"name"` - Setting Setting `edgedb:"setting"` - Conversations []Conversation `edgedb:"conversations"` - Usages []Usage `edgedb:"usages"` + ID edgedb.UUID `edgedb:"id"` + Setting Setting `edgedb:"setting"` } type Key struct { - ID edgedb.UUID `edgedb:"id"` - Name string `edgedb:"name"` - Key string `edgedb:"key"` - Date time.Time `edgedb:"date"` + ID edgedb.UUID `edgedb:"id"` + Name string `edgedb:"name"` + Company string `edgedb:"company"` + Key string `edgedb:"key"` + Date time.Time `edgedb:"date"` } type Setting struct { @@ -35,24 +32,27 @@ type Setting struct { } type Conversation struct { - ID edgedb.UUID `edgedb:"id"` - Name string `edgedb:"name"` - Areas []Area `edgedb:"areas"` + ID edgedb.UUID `edgedb:"id"` + Name string `edgedb:"name"` + Date time.Time `edgedb:"date"` + User User `edgedb:"user"` } type Area struct { - ID edgedb.UUID `edgedb:"id"` - Position int `edgedb:"position"` - Messages []Message `edgedb:"messages"` + ID edgedb.UUID `edgedb:"id"` + Position int `edgedb:"position"` + Conv Conversation `edgedb:"conversation"` } type Message struct { ID edgedb.UUID `edgedb:"id"` + Content string `edgedb:"content"` + Role string `edgedb:"role"` ModelID edgedb.OptionalStr `edgedb:"model_id"` Selected edgedb.OptionalBool `edgedb:"selected"` - Role string `edgedb:"role"` - Content string `edgedb:"content"` Date time.Time `edgedb:"date"` + Area Area `edgedb:"area"` + Conv Conversation `edgedb:"conversation"` } type Usage struct { @@ -109,10 +109,34 @@ func checkIfLogin() bool { return err == nil } -func insertArea() edgedb.UUID { - // Insert a new area. +func insertNewConversation() edgedb.UUID { var inserted struct{ id edgedb.UUID } err := edgeClient.QuerySingle(edgeCtx, ` + INSERT Conversation { + name := 'Default', + user := global currentUser + } + `, &inserted) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in insertNewConversation") + log.Fatal(err) + } + return inserted.id +} + +func insertArea() edgedb.UUID { + // If the Default conversation doesn't exist, create it. + err := edgeClient.QuerySingle(edgeCtx, ` + SELECT Conversation + FILTER .name = 'Default' AND .user = global currentUser + LIMIT 1 + `, nil) + if err != nil { + 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 INSERT Area { @@ -168,10 +192,10 @@ func insertBotMessage(content string, selected bool, model string) edgedb.UUID { content := $2, selected := $3, conversation := ( - SELECT Conversation - FILTER .name = 'Default' AND .user = global currentUser + SELECT Area + FILTER .id = $4 LIMIT 1 - ), + ).conversation, area := ( SELECT Area FILTER .id = $4 @@ -212,3 +236,18 @@ func getAllMessages() []Message { return messages } + +func getCurrentUserKeys() []Key { + var result []Key + err := edgeClient.Query(edgeCtx, "SELECT global currentUser.setting.keys", &result) + if err != nil { + fmt.Println("Error in edgedb.Query: in getCurrentUserKeys") + fmt.Println(err) + } + return result +} + +func checkIfHaveKey() bool { + keys := getCurrentUserKeys() + return keys != nil && len(keys) > 0 +} diff --git a/main.go b/main.go index 4663818..d64227e 100644 --- a/main.go +++ b/main.go @@ -40,9 +40,14 @@ func main() { app.Get("/generateMultipleMessages", GenerateMultipleMessages) app.Get("/messageContent", GetMessageContentHandler) + // Settings routes + app.Post("/addOpenaiKey", addOpenaiKey) + app.Post("/addAnthropicKey", addAnthropicKey) + // Popovers app.Get("/loadModelSelection", LoadModelSelectionHandler) app.Get("/loadUsageKPI", LoadUsageKPIHandler) + app.Get("/loadSettings", LoadSettingsHandler) // Authentication app.Get("/signin", handleUiSignIn) diff --git a/views/chat.html b/views/chat.html index c1a69b5..a65c365 100644 --- a/views/chat.html +++ b/views/chat.html @@ -1,14 +1,18 @@
- + +
- +
- + -
diff --git a/views/partials/message-bot.html b/views/partials/message-bot.html index dc80da9..be49dda 100644 --- a/views/partials/message-bot.html +++ b/views/partials/message-bot.html @@ -4,8 +4,8 @@
{% for message in Messages %}
- +
User Image diff --git a/views/partials/navbar.html b/views/partials/navbar.html index cf0f535..2f5ebde 100644 --- a/views/partials/navbar.html +++ b/views/partials/navbar.html @@ -13,13 +13,9 @@ diff --git a/views/partials/conversationsPopover.html b/views/partials/popover-conversations.html similarity index 100% rename from views/partials/conversationsPopover.html rename to views/partials/popover-conversations.html diff --git a/views/partials/modelsPopover.html b/views/partials/popover-models.html similarity index 100% rename from views/partials/modelsPopover.html rename to views/partials/popover-models.html diff --git a/views/partials/popover-settings.html b/views/partials/popover-settings.html new file mode 100644 index 0000000..09a94ed --- /dev/null +++ b/views/partials/popover-settings.html @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/views/partials/usagePopover.html b/views/partials/popover-usage.html similarity index 100% rename from views/partials/usagePopover.html rename to views/partials/popover-usage.html