Conversation menu

This commit is contained in:
Adrien Bouvais 2024-05-24 19:50:42 +02:00
parent 09193a227e
commit 01d04b3611
13 changed files with 173 additions and 177 deletions

36
Chat.go
View File

@ -594,6 +594,42 @@ func LoadModelSelectionHandler(c *fiber.Ctx) error {
return c.SendString(GenerateModelPopoverHTML(false)) return c.SendString(GenerateModelPopoverHTML(false))
} }
func GenerateConversationPopoverHTML(refresh bool) string {
var conversations []Conversation
err := edgeClient.Query(edgeCtx, `
SELECT Conversation {
name,
position
}
FILTER .user = global currentUser
ORDER BY .position
`, &conversations)
if err != nil {
panic(err)
}
out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-conversation.html")).Execute(pongo2.Context{
"Conversations": conversations,
"IsActive": refresh,
})
if err != nil {
panic(err)
}
return out
}
func LoadConversationSelectionHandler(c *fiber.Ctx) error {
if !checkIfLogin() || !checkIfHaveKey() {
return c.SendString("")
}
return c.SendString(GenerateConversationPopoverHTML(false))
}
func RefreshConversationSelectionHandler(c *fiber.Ctx) error {
return c.SendString(GenerateConversationPopoverHTML(true))
}
func LoadSettingsHandler(c *fiber.Ctx) error { func LoadSettingsHandler(c *fiber.Ctx) error {
if !checkIfLogin() { if !checkIfLogin() {
return c.SendString("") return c.SendString("")

View File

@ -39,10 +39,11 @@ type Setting struct {
} }
type Conversation struct { type Conversation struct {
ID edgedb.UUID `edgedb:"id"` ID edgedb.UUID `edgedb:"id"`
Name string `edgedb:"name"` Name string `edgedb:"name"`
Date time.Time `edgedb:"date"` Position int32 `edgedb:"position"`
User User `edgedb:"user"` Date time.Time `edgedb:"date"`
User User `edgedb:"user"`
} }
type Area struct { type Area struct {
@ -155,9 +156,15 @@ func addUsage(inputCost float32, outputCost float32, inputToken int32, outputTok
func insertNewConversation() edgedb.UUID { func insertNewConversation() edgedb.UUID {
var inserted struct{ id edgedb.UUID } var inserted struct{ id edgedb.UUID }
err := edgeClient.QuerySingle(edgeCtx, ` err := edgeClient.QuerySingle(edgeCtx, `
WITH
C := (
SELECT Conversation
FILTER .user = global currentUser
)
INSERT Conversation { INSERT Conversation {
name := 'Default', name := 'Default',
user := global currentUser user := global currentUser,
position := count(C) + 1
} }
`, &inserted) `, &inserted)
if err != nil { if err != nil {

View File

@ -37,6 +37,7 @@ module default {
type Conversation { type Conversation {
required name: str; required name: str;
required position: int32;
required user: User { required user: User {
on target delete delete source; on target delete delete source;
}; };

View File

@ -0,0 +1,9 @@
CREATE MIGRATION m1ospnjzsatmkntvczm5eu65omytyezeg3lanxeogtdqz2372t6cuq
ONTO m1cmvjy3ikuh5ii6b7l7gckttjmqk554llocwqx7n4aibtzngybvoq
{
ALTER TYPE default::Conversation {
CREATE REQUIRED PROPERTY position: std::int32 {
SET REQUIRED USING (<std::int32>{0});
};
};
};

View File

@ -86,6 +86,8 @@ func main() {
// Popovers // Popovers
app.Get("/loadModelSelection", LoadModelSelectionHandler) app.Get("/loadModelSelection", LoadModelSelectionHandler)
app.Get("/loadConversationSelection", LoadConversationSelectionHandler)
app.Get("/refreshConversationSelection", RefreshConversationSelectionHandler)
app.Get("/loadUsageKPI", LoadUsageKPIHandler) app.Get("/loadUsageKPI", LoadUsageKPIHandler)
app.Get("/loadKeys", LoadKeysHandler) app.Get("/loadKeys", LoadKeysHandler)
app.Get("/loadSettings", LoadSettingsHandler) app.Get("/loadSettings", LoadSettingsHandler)

BIN
static/icons/.DS_Store vendored

Binary file not shown.

View File

@ -119,3 +119,53 @@ svg text {
.message-bot:hover .message-button { .message-bot:hover .message-button {
opacity: 1; opacity: 1;
} }
.selected {
border: 2px solid #126d0f;
border-radius: 4px;
padding: 1px;
margin: 2px;
}
.unselected {
border-radius: 4px;
padding: 3px;
}
.shiftselected {
background: #126d0f;
border-radius: 4px;
padding: 1px;
margin: 2px;
}
.shift-pressed *::selection {
background: transparent;
}
input[type="range"].slider {
-webkit-appearance: none;
width: 80%;
background: transparent;
border: 1px solid #2c2c2c;
border-radius: 4px;
height: 6px;
border-radius: 2px;
}
input[type="range"].slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 2px;
background: #126d0f;
cursor: pointer;
}
input[type="range"].slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 2px;
background: #ffffff;
cursor: pointer;
}

View File

@ -9,8 +9,8 @@
id="chat-input-textarea"></textarea> id="chat-input-textarea"></textarea>
<div class="button-group"> <div class="button-group">
<hx hx-get="/loadSettings" 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="/loadKeys" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx-->
<hx hx-get="/loadUsageKPI" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx> <hx hx-get="/loadUsageKPI" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<hx hx-get="/loadConversationSelection" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<hx hx-get="/loadModelSelection" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx> <hx hx-get="/loadModelSelection" hx-trigger="load" hx-swap="outerHTML" hx-target="this"></hx>
<button {% if not IsLogin or not HaveKey %}style="display: none;" {%endif%} class="button is-small" <button {% if not IsLogin or not HaveKey %}style="display: none;" {%endif%} class="button is-small"
onclick="clearTextArea()" id="clear-button" hx-post="/clearChat" hx-swap="outerHTML" onclick="clearTextArea()" id="clear-button" hx-post="/clearChat" hx-swap="outerHTML"

View File

@ -0,0 +1,52 @@
<div class="dropdown is-up is-right {% if IsActive %} is-active {% endif %}" id="conversation-dropdown">
<div class="dropdown-trigger">
<button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu3"
hx-get="/refreshConversationSelection" hx-target="#conversation-dropdown" hx-swap="outerHTML">
<span class="icon">
<i class="fa-solid fa-comments"></i>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu3" role="menu">
<div class="dropdown-content">
<div class="dropdown-item" id="models-list">
<div id="conversation-list">
{% for Conversation in Conversations %}
<div class="icon-text has-text unselected" data-id="{{ Converrsation.ID.String() }}"
style="cursor: pointer;" onclick="toggleSelection(this)">
<span>{{ Conversation.Name }}</span>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% if not IsActive %}
<script>
var sortable = new Sortable(document.getElementById('conversation-list'), {
animation: 150,
onEnd: function (evt) {
var items = evt.to.children;
var updates = [];
for (var i = 0; i < items.length; i++) {
var id = items[i].getAttribute('data-id');
var position = i + 1;
updates.push({ id: id, position: position });
}
var xhr = new XMLHttpRequest();
xhr.open('POST', '/updateConversationPositionBatch', true);
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.send(JSON.stringify(updates));
}
});
document.addEventListener('click', function (event) {
if (!document.getElementById('conversation-dropdown').contains(event.target)) {
document.getElementById('conversation-dropdown').classList.remove('is-active');
}
});
</script>
{% endif %}

View File

@ -1,27 +0,0 @@
<div class="dropdown is-hoverable is-up">
<div class="dropdown-trigger">
<button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu4">
<span>Conversations</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu4" role="menu">
<div class="dropdown-content">
<div class="dropdown-item">
{% for conversation in Conversations %}
<div class="content">
<h5 class="subtitle is-5">{{ CompanyInfo.Name }}</h5>
{% for ModelInfo in CompanyInfo.ModelInfos %}
<div class="field">
<label class="checkbox">
<input {%if ModelInfo.ID in CheckedModels %}checked{% endif %} type="checkbox"
name="model-check-{{ ModelInfo.ID }}" value="{{ ModelInfo.ID }}" />
{{ ModelInfo.Name }}
</label>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>

View File

@ -1,88 +0,0 @@
<div class="dropdown is-hoverable is-up is-right">
<div class="dropdown-trigger">
<button class="button is-small {% if not AnyExists %} is-danger{% endif %}" aria-haspopup="true"
aria-controls="dropdown-menu4">
<span class="icon"><i class="fa-solid fa-key"></i></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" hx-post="/addKeys" hx-trigger="submit" hx-target="#api-keys-status">
<div class="field has-addons">
<p class="control has-icons-left is-expanded">
<input class="input is-small {% if OpenaiExists %}is-success{% endif %}" type="text"
{%if not IsLogin %}disabled{% endif %} placeholder="OpenAI API key"
name="openai_key" autocomplete="off">
<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 {% if AnthropicExists %}is-success{% endif %}" type="text"
{% if not IsLogin %}disabled{% endif %} placeholder="Anthropic API key"
name="anthropic_key" autocomplete="off">
<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 {% if MistralExists %}is-success{% endif %}" type="text"
{%if not IsLogin %}disabled{% endif %} placeholder="Mistral API key"
name="mistral_key" autocomplete="off">
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
<div class="field has-addons" title="Gemini is unavailable because of Europe">
<p class="control has-icons-left is-expanded">
<input class="input is-small {% if GoogleExists %}is-success{% endif %}" type="text"
disabled placeholder="Google API key" name="google_key" autocomplete="off">
<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 {% if GooseaiExists %}is-success{% endif %}" type="text"
placeholder="Gooseai API key" {%if not IsLogin %}disabled{% endif %}
name="goose_key" autocomplete="off">
<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 {% if GroqExists %}is-success{% endif %}" type="text"
placeholder="Groq API key" {%if not IsLogin %}disabled{% endif %} name="groq_key"
autocomplete="off">
<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">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>Save</span>
</button>
</p>
</div>
</form>
<p id="api-keys-status"></p>
</div>
</div>
</div>
</div>
</div>

View File

@ -164,55 +164,3 @@
} }
}); });
</script> </script>
<style>
.selected {
border: 2px solid #126d0f;
border-radius: 4px;
padding: 1px;
margin: 2px;
}
.unselected {
border-radius: 4px;
padding: 3px;
}
.shiftselected {
background: #126d0f;
border-radius: 4px;
padding: 1px;
margin: 2px;
}
.shift-pressed *::selection {
background: transparent;
}
input[type="range"].slider {
-webkit-appearance: none;
width: 80%;
background: transparent;
border: 1px solid #2c2c2c;
border-radius: 4px;
height: 6px;
border-radius: 2px;
}
input[type="range"].slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 2px;
background: #126d0f;
cursor: pointer;
}
input[type="range"].slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 2px;
background: #ffffff;
cursor: pointer;
}
</style>

View File

@ -1,11 +1,11 @@
<div class="dropdown is-hoverable is-up is-right"> <div class="dropdown is-up is-right" id="settings-dropdown">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button is-small {% if not AnyExists %} is-danger{% endif %}" aria-haspopup="true" <button class="button is-small {% if not AnyExists %} is-danger{% endif %}" aria-haspopup="true"
aria-controls="dropdown-menu4"> aria-controls="dropdown-menu3" onclick="this.parentElement.parentElement.classList.toggle('is-active')">
<span class="icon"><i class="fa-solid fa-bars"></i></i></span> <span class="icon"><i class="fa-solid fa-bars"></i></i></span>
</button> </button>
</div> </div>
<div class="dropdown-menu" id="dropdown-menu4" role="menu"> <div class="dropdown-menu" id="dropdown-menu3" role="menu">
<div class="dropdown-content"> <div class="dropdown-content">
<div class="dropdown-item"> <div class="dropdown-item">
<div class="field"> <div class="field">
@ -120,6 +120,12 @@
</div> </div>
<script> <script>
document.addEventListener('click', function (event) {
if (!document.getElementById('settings-dropdown').contains(event.target)) {
document.getElementById('settings-dropdown').classList.remove('is-active');
}
});
document.getElementById('toggle-keys-button').addEventListener('click', function () { document.getElementById('toggle-keys-button').addEventListener('click', function () {
// Do not take the id="save-field" // Do not take the id="save-field"
var fields = document.querySelectorAll("#api-keys-form .field.has-addons"); var fields = document.querySelectorAll("#api-keys-form .field.has-addons");