Cqn enter key

This commit is contained in:
Adrien Bouvais 2024-05-10 22:30:23 +02:00
parent ed64fc01d8
commit d2556a4a6d
12 changed files with 479 additions and 42 deletions

59
Chat.go
View File

@ -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 := `
<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 := []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: "<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})
botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0, "NotClickable": !true})
if err != nil {
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 := `To start using JADE, please enter at least one key in the settings.`
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 := []NextMessage{}
nextMsg := NextMessage{
Icon: "bouvai2", // Assuming Icon is a field you want to include from Message
Content: "<br>" + 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)
}

View File

@ -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 = <str>$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 = <str>$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: "<br>" + 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 := <str>$0,
key := <str>$1,
name := <str>$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: "<br>" + 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)
}

View File

@ -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 = <str>$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 = <str>$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: "<br>" + 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 = <str>$0 AND .key = <str>$1
SET {
key := <str>$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 := <str>$0,
key := <str>$1,
name := <str>$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: "<br> 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)
}

View File

@ -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 := <str>$2,
selected := <bool>$3,
conversation := (
SELECT Conversation
FILTER .name = 'Default' AND .user = global currentUser
SELECT Area
FILTER .id = <uuid>$4
LIMIT 1
),
).conversation,
area := (
SELECT Area
FILTER .id = <uuid>$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
}

View File

@ -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)

View File

@ -1,14 +1,18 @@
<div class="chat-container mt-5">
<hx hx-get="/loadChat" hx-trigger="load" hx-swap="outerHTML"></hx>
<hx hx-get="/loadChat" hx-trigger="load once" hx-swap="outerHTML"></hx>
<div class="chat-input-container mb-5">
<div class="textarea-wrapper">
<textarea class="textarea" placeholder="Type your message here..." name="message"></textarea>
<textarea {% if not IsLogin or not HaveKey %}disabled{% endif %} class="textarea"
placeholder="Type your message here..." name="message"></textarea>
<div class="button-group">
<hx hx-get="/loadModelSelection" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<hx hx-get="/loadSettings" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<hx hx-get="/loadUsageKPI" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<button type="submit" class="send-button button is-primary is-small" hx-post="/requestMultipleMessages"
<hx hx-get="/loadModelSelection" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<button {% if not IsLogin or not HaveKey %}disabled{% endif %} type="submit"
class="send-button button is-primary is-small" hx-post="/requestMultipleMessages"
hx-swap="beforeend settle:200ms" hx-target="#chat-messages" id="chat-input-send-btn"
class="chat-input" hx-include="[name='message'], [name^='model-check-']">Send</button>
</div>

View File

@ -4,8 +4,8 @@
<div class='rows'>
{% for message in Messages %}
<div class='row is-full mt-1'>
<a href="#" hx-get="/messageContent?id={{ message.Id }}" class="is-clickable"
hx-target="#content-{{ ConversationAreaId }}" onclick="toggleGrayscale(this)">
<a {% if NotClickable %} href="#" hx-get="/messageContent?id={{ message.Id }}" class="is-clickable"
onclick="toggleGrayscale(this)" hx-target="#content-{{ ConversationAreaId }}" {% endif %}>
<figure class="image is-48x48" style="flex-shrink: 0;">
<img src="icons/{{ message.Icon }}.png" alt="User Image" {% if message.Hidden %}
style="filter: grayscale(100%);" {% endif %} title="{{ message.Name }}">

View File

@ -13,13 +13,9 @@
<div class="navbar-item">
<div class="buttons">
{% if IsLogin %}
<a class="button is-light is-small" href="/signout">
<a class="button is-small is-info is-outlined" href="/signout">
Log out
</a>
{% else %}
<a class="button is-light is-small" href="/signin">
Log in
</a>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,42 @@
<div class="dropdown is-hoverable is-up">
<div class="dropdown-trigger">
<button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu4">
<span>Settings</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu4" role="menu">
<div class="dropdown-content">
<div class="dropdown-item">
<div class="field">
<form id="api-keys-form" method="post" action="/addKeys" hx-trigger="submit"
hx-target="#chat-messages" hx-swap="beforeend">
<div class="field has-addons">
<p class="control has-icons-left is-expanded">
<input class="input is-small" type="text" placeholder="OpenAI API key" name="openai_key"
autocomplete="off" {% if not IsLogin %}disabled{% endif %}>
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
<div class="field has-addons">
<p class="control has-icons-left is-expanded">
<input class="input is-small" type="text" placeholder="Anthropic API key"
name="anthropic_key" autocomplete="off" {% if not IsLogin %}disabled{% endif %}>
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
<div class="field has-addons">
<p class="control">
<button {% if not IsLogin %}disabled{% endif %} type="submit"
class="button is-small">Submit</button>
</p>
</div>
</form>
</div>
</div>
</div>
</div>
</div>