Barely working per request response
This commit is contained in:
parent
bb8f53a018
commit
1b73a97ff1
30
Chat.go
30
Chat.go
@ -15,11 +15,6 @@ import (
|
|||||||
func ChatPageHandler(c *fiber.Ctx) error {
|
func ChatPageHandler(c *fiber.Ctx) error {
|
||||||
authCookie := c.Cookies("jade-edgedb-auth-token", "")
|
authCookie := c.Cookies("jade-edgedb-auth-token", "")
|
||||||
|
|
||||||
go func() {
|
|
||||||
fmt.Println("Sending test event")
|
|
||||||
sseChanel.SendEvent("test", "Hello from server")
|
|
||||||
}()
|
|
||||||
|
|
||||||
if authCookie != "" && !checkIfLogin() {
|
if authCookie != "" && !checkIfLogin() {
|
||||||
edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie})
|
edgeClient = edgeClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": authCookie})
|
||||||
}
|
}
|
||||||
@ -141,19 +136,16 @@ func generateChatHTML() string {
|
|||||||
|
|
||||||
func GetMessageContentHandler(c *fiber.Ctx) error {
|
func GetMessageContentHandler(c *fiber.Ctx) error {
|
||||||
messageId := c.FormValue("id")
|
messageId := c.FormValue("id")
|
||||||
onlyContent := c.FormValue("onlyContent")
|
onlyContent := c.FormValue("onlyContent") // To init the text area of the edit message form
|
||||||
|
|
||||||
messageUUID, err := edgedb.ParseUUID(messageId)
|
messageUUID, _ := edgedb.ParseUUID(messageId)
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error in uuid.FromString: in DeleteMessageHandler")
|
|
||||||
fmt.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedMessage Message
|
var selectedMessage Message
|
||||||
err = edgeClient.QuerySingle(context.Background(), `
|
err := edgeClient.QuerySingle(context.Background(), `
|
||||||
SELECT Message {
|
SELECT Message {
|
||||||
model_id,
|
model_id,
|
||||||
content
|
content,
|
||||||
|
area
|
||||||
}
|
}
|
||||||
FILTER
|
FILTER
|
||||||
.id = <uuid>$0;
|
.id = <uuid>$0;
|
||||||
@ -162,12 +154,11 @@ func GetMessageContentHandler(c *fiber.Ctx) error {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
modelID, _ := selectedMessage.ModelID.Get()
|
|
||||||
|
|
||||||
if onlyContent == "true" {
|
if onlyContent == "true" {
|
||||||
return c.SendString(markdownToHTML(selectedMessage.Content))
|
return c.SendString(markdownToHTML(selectedMessage.Content))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modelID, _ := selectedMessage.ModelID.Get()
|
||||||
out := "<div class='message-header'>"
|
out := "<div class='message-header'>"
|
||||||
out += "<p>"
|
out += "<p>"
|
||||||
out += model2Name(modelID)
|
out += model2Name(modelID)
|
||||||
@ -303,15 +294,6 @@ func RedoMessageHandler(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedModelIds := []string{}
|
|
||||||
for ModelInfo := range ModelsInfos {
|
|
||||||
out := c.FormValue("model-check-" + ModelsInfos[ModelInfo].ID)
|
|
||||||
if out != "" {
|
|
||||||
selectedModelIds = append(selectedModelIds, ModelsInfos[ModelInfo].ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastSelectedModelIds = removeDuplicate(selectedModelIds)
|
|
||||||
|
|
||||||
return c.SendString(messageOut)
|
return c.SendString(messageOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
227
Request.go
227
Request.go
@ -28,62 +28,69 @@ type CompanyInfo struct {
|
|||||||
ModelInfos []ModelInfo
|
ModelInfos []ModelInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectedModel struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
var CompanyInfos []CompanyInfo
|
var CompanyInfos []CompanyInfo
|
||||||
var ModelsInfos []ModelInfo
|
var ModelsInfos []ModelInfo
|
||||||
|
|
||||||
type MultipleModelsCompletionRequest struct {
|
|
||||||
ModelIds []string
|
|
||||||
Messages []Message
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
type BotContentMessage struct {
|
|
||||||
Content string
|
|
||||||
Hidden bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastSelectedModelIds []string
|
|
||||||
|
|
||||||
func addUsage(inputCost float32, outputCost float32, inputToken int32, outputToken int32, modelID string) {
|
|
||||||
// Create a new usage
|
|
||||||
err := edgeClient.Execute(edgeCtx, `
|
|
||||||
INSERT Usage {
|
|
||||||
input_cost := <float32>$0,
|
|
||||||
output_cost := <float32>$1,
|
|
||||||
input_token := <int32>$2,
|
|
||||||
output_token := <int32>$3,
|
|
||||||
model_id := <str>$4,
|
|
||||||
user := global currentUser
|
|
||||||
}
|
|
||||||
`, inputCost, outputCost, inputToken, outputToken, modelID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error in edgedb.QuerySingle: in addUsage")
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateMultipleMessages(c *fiber.Ctx) error {
|
func GenerateMultipleMessages(c *fiber.Ctx) error {
|
||||||
|
message := c.FormValue("message", "")
|
||||||
|
selectedModelIds := []string{}
|
||||||
|
for ModelInfo := range ModelsInfos {
|
||||||
|
out := c.FormValue("model-check-" + ModelsInfos[ModelInfo].ID)
|
||||||
|
if out != "" {
|
||||||
|
selectedModelIds = append(selectedModelIds, ModelsInfos[ModelInfo].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, position := insertArea()
|
||||||
|
messageID := insertUserMessage(message)
|
||||||
|
|
||||||
|
out := ""
|
||||||
|
messageOut, _ := userTmpl.Execute(pongo2.Context{"Content": markdownToHTML(message), "ID": messageID.String()})
|
||||||
|
out += messageOut
|
||||||
|
|
||||||
|
var selectedModels []SelectedModel
|
||||||
|
for i := range selectedModelIds {
|
||||||
|
selectedModels = append(selectedModels, SelectedModel{ID: selectedModelIds[i], Name: model2Name(selectedModelIds[i]), Icon: model2Icon(selectedModelIds[i])})
|
||||||
|
}
|
||||||
|
|
||||||
|
messageOut, _ = botTmpl.Execute(pongo2.Context{"IsPlaceholder": true, "selectedModels": selectedModels, "ConversationAreaId": position + 1})
|
||||||
|
out += messageOut
|
||||||
|
|
||||||
|
go HandleGenerateMultipleMessages(selectedModelIds)
|
||||||
|
|
||||||
|
return c.SendString(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGenerateMultipleMessages(selectedModelIds []string) {
|
||||||
insertArea()
|
insertArea()
|
||||||
|
|
||||||
// Create a wait group to synchronize the goroutines
|
// Create a wait group to synchronize the goroutines
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Add the length of lastSelectedModelIds goroutines to the wait group
|
// Add the length of selectedModelIds goroutines to the wait group
|
||||||
wg.Add(len(lastSelectedModelIds))
|
wg.Add(len(selectedModelIds))
|
||||||
|
|
||||||
for i := range lastSelectedModelIds {
|
// Create a channel to receive the index of the first completed goroutine
|
||||||
|
firstDone := make(chan int, 1)
|
||||||
|
|
||||||
|
for i := range selectedModelIds {
|
||||||
idx := i
|
idx := i
|
||||||
go func() {
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
// Create a context with a 1-minute timeout
|
// Create a context with a 1-minute timeout
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel() // Ensure the context is cancelled to free resources
|
defer cancel() // Ensure the context is cancelled to free resources
|
||||||
|
|
||||||
// Use a channel to signal the completion of addxxxMessage
|
|
||||||
done := make(chan struct{}, 1)
|
|
||||||
|
|
||||||
// Determine which message function to call based on the model
|
// Determine which message function to call based on the model
|
||||||
var addMessageFunc func(modelID string, selected bool) edgedb.UUID
|
var addMessageFunc func(modelID string, selected bool) edgedb.UUID
|
||||||
switch model2Icon(lastSelectedModelIds[idx]) {
|
switch model2Icon(selectedModelIds[idx]) {
|
||||||
case "openai":
|
case "openai":
|
||||||
addMessageFunc = addOpenaiMessage
|
addMessageFunc = addOpenaiMessage
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
@ -94,62 +101,110 @@ func GenerateMultipleMessages(c *fiber.Ctx) error {
|
|||||||
addMessageFunc = addGroqMessage
|
addMessageFunc = addGroqMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the selected addMessageFunc in a goroutine
|
var messageID edgedb.UUID
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if addMessageFunc != nil {
|
if addMessageFunc != nil {
|
||||||
addMessageFunc(lastSelectedModelIds[idx], idx == 0)
|
messageID = addMessageFunc(selectedModelIds[idx], idx == 0)
|
||||||
}
|
}
|
||||||
done <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Use select to wait on multiple channel operations
|
var message Message
|
||||||
|
err := edgeClient.QuerySingle(edgeCtx, `
|
||||||
|
SELECT Message {
|
||||||
|
model_id,
|
||||||
|
content,
|
||||||
|
area
|
||||||
|
}
|
||||||
|
FILTER .id = <uuid>$0;
|
||||||
|
`, &message, messageID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in edgedb.QuerySingle: in GenerateMultipleMessages")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
modelID, _ := message.ModelID.Get()
|
||||||
|
|
||||||
|
var area Area
|
||||||
|
err = edgeClient.QuerySingle(edgeCtx, `
|
||||||
|
SELECT Area {
|
||||||
|
position
|
||||||
|
}
|
||||||
|
FILTER .id = <uuid>$0;
|
||||||
|
`, &area, message.Area.ID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in edgedb.QuerySingle: in GenerateMultipleMessages")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(area)
|
||||||
|
|
||||||
|
// Check if the context's deadline is exceeded
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done(): // Context's deadline is exceeded
|
case <-ctx.Done():
|
||||||
// Insert a bot message indicating a timeout
|
// The context's deadline was exceeded
|
||||||
insertBotMessage(lastSelectedModelIds[idx]+" too long to answer", idx == 0, lastSelectedModelIds[idx])
|
fmt.Printf("Goroutine %d timed out\n", idx)
|
||||||
case <-done: // addMessageFunc completed within the deadline
|
default:
|
||||||
// No action needed, the function completed successfully
|
// Send the index of the completed goroutine to the firstDone channel
|
||||||
|
select {
|
||||||
|
case firstDone <- idx:
|
||||||
|
// Generate the HTML content
|
||||||
|
out := "<div class='message-header'>"
|
||||||
|
out += "<p>"
|
||||||
|
out += model2Name(modelID)
|
||||||
|
out += " </p>"
|
||||||
|
out += "</div>"
|
||||||
|
out += "<div class='message-body'>"
|
||||||
|
out += " <ct class='content'>"
|
||||||
|
out += markdownToHTML(message.Content)
|
||||||
|
out += " </ct>"
|
||||||
|
out += "</div>"
|
||||||
|
|
||||||
|
fmt.Println("Sending event from first")
|
||||||
|
fmt.Println("swapContent-" + fmt.Sprintf("%d", area.Position))
|
||||||
|
|
||||||
|
// Send Content event
|
||||||
|
sseChanel.SendEvent(
|
||||||
|
"swapContent-"+fmt.Sprintf("%d", area.Position),
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
|
||||||
|
out, err := modelSelecBtnTmpl.Execute(map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in modelSelecBtnTmpl.Execute: in GenerateMultipleMessages")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Content event
|
||||||
|
sseChanel.SendEvent(
|
||||||
|
"swapSelectionBtn-"+modelID,
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send Icon Swap event
|
||||||
|
sseChanel.SendEvent(
|
||||||
|
"swapIcon-"+fmt.Sprintf("%d", area.Position),
|
||||||
|
`<img src="icons/`+model2Icon(modelID)+`.png" alt="User Image">`,
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
out, err := modelSelecBtnTmpl.Execute(map[string]interface{}{
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in modelSelecBtnTmpl.Execute: in GenerateMultipleMessages")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(("Sending event"))
|
||||||
|
|
||||||
|
// Send Content event
|
||||||
|
sseChanel.SendEvent(
|
||||||
|
"swapSelectionBtn-"+modelID,
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all goroutines to finish
|
// Wait for all goroutines to finish
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
fmt.Println("Done!")
|
|
||||||
|
|
||||||
return c.SendString(generateChatHTML())
|
|
||||||
}
|
|
||||||
|
|
||||||
func RequestMultipleMessagesHandler(c *fiber.Ctx) error {
|
|
||||||
message := c.FormValue("message", "")
|
|
||||||
|
|
||||||
selectedModelIds := []string{}
|
|
||||||
for ModelInfo := range ModelsInfos {
|
|
||||||
out := c.FormValue("model-check-" + ModelsInfos[ModelInfo].ID)
|
|
||||||
if out != "" {
|
|
||||||
selectedModelIds = append(selectedModelIds, ModelsInfos[ModelInfo].ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastSelectedModelIds = selectedModelIds
|
|
||||||
|
|
||||||
out := RequestMultipleMessages(message, selectedModelIds)
|
|
||||||
|
|
||||||
return c.SendString(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RequestMultipleMessages(message string, selectedModelIds []string) string {
|
|
||||||
// Add an Area with the user message inside
|
|
||||||
insertArea()
|
|
||||||
messageID := insertUserMessage(message)
|
|
||||||
|
|
||||||
out := ""
|
|
||||||
messageOut, _ := userTmpl.Execute(pongo2.Context{"Content": markdownToHTML(message), "ID": messageID.String()})
|
|
||||||
out += messageOut
|
|
||||||
|
|
||||||
messageOut, _ = botTmpl.Execute(pongo2.Context{"IsPlaceholder": true})
|
|
||||||
out += messageOut
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,11 @@ import (
|
|||||||
|
|
||||||
type AnthropicChatCompletionRequest struct {
|
type AnthropicChatCompletionRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []AnthropicMessage `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnthropicMessage struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnthropicChatCompletionResponse struct {
|
type AnthropicChatCompletionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Content []AnthropicContentItem `json:"content"`
|
Content []AnthropicContentItem `json:"content"`
|
||||||
@ -104,30 +99,10 @@ func addAnthropicMessage(modelID string, selected bool) edgedb.UUID {
|
|||||||
return edgedb.UUID{}
|
return edgedb.UUID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EdgeMessages2AnthropicMessages(messages []Message) []AnthropicMessage {
|
|
||||||
AnthropicMessages := make([]AnthropicMessage, len(messages))
|
|
||||||
for i, msg := range messages {
|
|
||||||
var role string
|
|
||||||
switch msg.Role {
|
|
||||||
case "user":
|
|
||||||
role = "user"
|
|
||||||
case "bot":
|
|
||||||
role = "assistant"
|
|
||||||
default:
|
|
||||||
role = "system"
|
|
||||||
}
|
|
||||||
AnthropicMessages[i] = AnthropicMessage{
|
|
||||||
Role: role,
|
|
||||||
Content: msg.Content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AnthropicMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnthropicKey(apiKey string) bool {
|
func TestAnthropicKey(apiKey string) bool {
|
||||||
url := "https://api.anthropic.com/v1/messages"
|
url := "https://api.anthropic.com/v1/messages"
|
||||||
|
|
||||||
AnthropicMessages := []AnthropicMessage{
|
AnthropicMessages := []Message{
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
@ -195,11 +170,9 @@ func RequestAnthropic(model string, messages []Message, maxTokens int, temperatu
|
|||||||
|
|
||||||
url := "https://api.anthropic.com/v1/messages"
|
url := "https://api.anthropic.com/v1/messages"
|
||||||
|
|
||||||
AnthropicMessages := EdgeMessages2AnthropicMessages(messages)
|
|
||||||
|
|
||||||
requestBody := AnthropicChatCompletionRequest{
|
requestBody := AnthropicChatCompletionRequest{
|
||||||
Model: model,
|
Model: model,
|
||||||
Messages: AnthropicMessages,
|
Messages: ChangeRoleBot2Assistant(messages),
|
||||||
MaxTokens: maxTokens,
|
MaxTokens: maxTokens,
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
}
|
}
|
||||||
|
@ -12,15 +12,10 @@ import (
|
|||||||
|
|
||||||
type GroqChatCompletionRequest struct {
|
type GroqChatCompletionRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []GroqMessage `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroqMessage struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GroqChatCompletionResponse struct {
|
type GroqChatCompletionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
@ -37,7 +32,7 @@ type GroqUsage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GroqChoice struct {
|
type GroqChoice struct {
|
||||||
Message GroqMessage `json:"message"`
|
Message Message `json:"message"`
|
||||||
FinishReason string `json:"finish_reason"`
|
FinishReason string `json:"finish_reason"`
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
}
|
}
|
||||||
@ -106,31 +101,11 @@ func addGroqMessage(modelID string, selected bool) edgedb.UUID {
|
|||||||
return edgedb.UUID{}
|
return edgedb.UUID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EdgeMessages2GroqMessages(messages []Message) []GroqMessage {
|
|
||||||
groqMessages := make([]GroqMessage, len(messages))
|
|
||||||
for i, msg := range messages {
|
|
||||||
var role string
|
|
||||||
switch msg.Role {
|
|
||||||
case "user":
|
|
||||||
role = "user"
|
|
||||||
case "bot":
|
|
||||||
role = "assistant"
|
|
||||||
default:
|
|
||||||
role = "system"
|
|
||||||
}
|
|
||||||
groqMessages[i] = GroqMessage{
|
|
||||||
Role: role,
|
|
||||||
Content: msg.Content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return groqMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGroqKey(apiKey string) bool {
|
func TestGroqKey(apiKey string) bool {
|
||||||
url := "https://api.groq.com/openai/v1/chat/completions"
|
url := "https://api.groq.com/openai/v1/chat/completions"
|
||||||
|
|
||||||
// Convert messages to Qroq format
|
// Convert messages to Qroq format
|
||||||
groqMessages := []GroqMessage{
|
groqMessages := []Message{
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
@ -199,12 +174,9 @@ func RequestGroq(model string, messages []Message, temperature float64) (GroqCha
|
|||||||
|
|
||||||
url := "https://api.groq.com/openai/v1/chat/completions"
|
url := "https://api.groq.com/openai/v1/chat/completions"
|
||||||
|
|
||||||
// Convert messages to Qroq format
|
|
||||||
groqMessages := EdgeMessages2GroqMessages(messages)
|
|
||||||
|
|
||||||
requestBody := GroqChatCompletionRequest{
|
requestBody := GroqChatCompletionRequest{
|
||||||
Model: model,
|
Model: model,
|
||||||
Messages: groqMessages,
|
Messages: ChangeRoleBot2Assistant(messages),
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,15 +12,9 @@ import (
|
|||||||
|
|
||||||
type MistralChatCompletionRequest struct {
|
type MistralChatCompletionRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []MistralMessage `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MistralMessage struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MistralChatCompletionResponse struct {
|
type MistralChatCompletionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
@ -37,7 +31,7 @@ type MistralUsage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MistralChoice struct {
|
type MistralChoice struct {
|
||||||
Message MistralMessage `json:"message"`
|
Message Message `json:"message"`
|
||||||
FinishReason string `json:"finish_reason"`
|
FinishReason string `json:"finish_reason"`
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
}
|
}
|
||||||
@ -139,31 +133,11 @@ func addMistralMessage(modelID string, selected bool) edgedb.UUID {
|
|||||||
return edgedb.UUID{}
|
return edgedb.UUID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EdgeMessages2MistralMessages(messages []Message) []MistralMessage {
|
|
||||||
mistralMessages := make([]MistralMessage, len(messages))
|
|
||||||
for i, msg := range messages {
|
|
||||||
var role string
|
|
||||||
switch msg.Role {
|
|
||||||
case "user":
|
|
||||||
role = "user"
|
|
||||||
case "bot":
|
|
||||||
role = "assistant"
|
|
||||||
default:
|
|
||||||
role = "system"
|
|
||||||
}
|
|
||||||
mistralMessages[i] = MistralMessage{
|
|
||||||
Role: role,
|
|
||||||
Content: msg.Content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mistralMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMistralKey(apiKey string) bool {
|
func TestMistralKey(apiKey string) bool {
|
||||||
url := "https://api.mistral.ai/v1/chat/completions"
|
url := "https://api.mistral.ai/v1/chat/completions"
|
||||||
|
|
||||||
// Convert messages to Mistral format
|
// Convert messages to Mistral format
|
||||||
mistralMessages := []MistralMessage{
|
mistralMessages := []Message{
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
@ -237,12 +211,9 @@ func RequestMistral(model string, messages []Message, temperature float64) (Mist
|
|||||||
|
|
||||||
url := "https://api.mistral.ai/v1/chat/completions"
|
url := "https://api.mistral.ai/v1/chat/completions"
|
||||||
|
|
||||||
// Convert messages to OpenAI format
|
|
||||||
mistralMessages := EdgeMessages2MistralMessages(messages)
|
|
||||||
|
|
||||||
requestBody := MistralChatCompletionRequest{
|
requestBody := MistralChatCompletionRequest{
|
||||||
Model: model,
|
Model: model,
|
||||||
Messages: mistralMessages,
|
Messages: ChangeRoleBot2Assistant(messages),
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,15 +12,10 @@ import (
|
|||||||
|
|
||||||
type OpenaiChatCompletionRequest struct {
|
type OpenaiChatCompletionRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []OpenaiMessage `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenaiMessage struct {
|
|
||||||
Role string `json:"role"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenaiChatCompletionResponse struct {
|
type OpenaiChatCompletionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Object string `json:"object"`
|
Object string `json:"object"`
|
||||||
@ -37,7 +32,7 @@ type OpenaiUsage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OpenaiChoice struct {
|
type OpenaiChoice struct {
|
||||||
Message OpenaiMessage `json:"message"`
|
Message Message `json:"message"`
|
||||||
FinishReason string `json:"finish_reason"`
|
FinishReason string `json:"finish_reason"`
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
}
|
}
|
||||||
@ -117,31 +112,11 @@ func addOpenaiMessage(modelID string, selected bool) edgedb.UUID {
|
|||||||
return edgedb.UUID{}
|
return edgedb.UUID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EdgeMessages2OpenaiMessages(messages []Message) []OpenaiMessage {
|
|
||||||
openaiMessages := make([]OpenaiMessage, len(messages))
|
|
||||||
for i, msg := range messages {
|
|
||||||
var role string
|
|
||||||
switch msg.Role {
|
|
||||||
case "user":
|
|
||||||
role = "user"
|
|
||||||
case "bot":
|
|
||||||
role = "assistant"
|
|
||||||
default:
|
|
||||||
role = "system"
|
|
||||||
}
|
|
||||||
openaiMessages[i] = OpenaiMessage{
|
|
||||||
Role: role,
|
|
||||||
Content: msg.Content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return openaiMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpenaiKey(apiKey string) bool {
|
func TestOpenaiKey(apiKey string) bool {
|
||||||
url := "https://api.openai.com/v1/chat/completions"
|
url := "https://api.openai.com/v1/chat/completions"
|
||||||
|
|
||||||
// Convert messages to OpenAI format
|
// Convert messages to OpenAI format
|
||||||
openaiMessages := []OpenaiMessage{
|
openaiMessages := []Message{
|
||||||
{
|
{
|
||||||
Role: "user",
|
Role: "user",
|
||||||
Content: "Hello",
|
Content: "Hello",
|
||||||
@ -208,12 +183,9 @@ func RequestOpenai(model string, messages []Message, temperature float64) (Opena
|
|||||||
|
|
||||||
url := "https://api.openai.com/v1/chat/completions"
|
url := "https://api.openai.com/v1/chat/completions"
|
||||||
|
|
||||||
// Convert messages to OpenAI format
|
|
||||||
openaiMessages := EdgeMessages2OpenaiMessages(messages)
|
|
||||||
|
|
||||||
requestBody := OpenaiChatCompletionRequest{
|
requestBody := OpenaiChatCompletionRequest{
|
||||||
Model: model,
|
Model: model,
|
||||||
Messages: openaiMessages,
|
Messages: ChangeRoleBot2Assistant(messages),
|
||||||
Temperature: temperature,
|
Temperature: temperature,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
38
database.go
38
database.go
@ -40,7 +40,7 @@ type Conversation struct {
|
|||||||
|
|
||||||
type Area struct {
|
type Area struct {
|
||||||
ID edgedb.UUID `edgedb:"id"`
|
ID edgedb.UUID `edgedb:"id"`
|
||||||
Position int `edgedb:"position"`
|
Position int64 `edgedb:"position"`
|
||||||
Conv Conversation `edgedb:"conversation"`
|
Conv Conversation `edgedb:"conversation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +111,24 @@ func checkIfLogin() bool {
|
|||||||
return err == nil
|
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 := <float32>$0,
|
||||||
|
output_cost := <float32>$1,
|
||||||
|
input_token := <int32>$2,
|
||||||
|
output_token := <int32>$3,
|
||||||
|
model_id := <str>$4,
|
||||||
|
user := global currentUser
|
||||||
|
}
|
||||||
|
`, inputCost, outputCost, inputToken, outputToken, modelID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in edgedb.QuerySingle: in addUsage")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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, `
|
||||||
@ -126,7 +144,7 @@ func insertNewConversation() edgedb.UUID {
|
|||||||
return inserted.id
|
return inserted.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertArea() edgedb.UUID {
|
func insertArea() (edgedb.UUID, int64) {
|
||||||
// If the Default conversation doesn't exist, create it.
|
// If the Default conversation doesn't exist, create it.
|
||||||
var convExists bool
|
var convExists bool
|
||||||
edgeClient.QuerySingle(edgeCtx, `
|
edgeClient.QuerySingle(edgeCtx, `
|
||||||
@ -157,7 +175,21 @@ func insertArea() edgedb.UUID {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return inserted.id
|
var positionSet struct{ position int64 }
|
||||||
|
err = edgeClient.QuerySingle(edgeCtx, `
|
||||||
|
SELECT Area {
|
||||||
|
position
|
||||||
|
}
|
||||||
|
FILTER .conversation.name = 'Default' AND .conversation.user = global currentUser
|
||||||
|
ORDER BY .position
|
||||||
|
LIMIT 1
|
||||||
|
`, &positionSet)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in edgedb.QuerySingle: in insertArea")
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inserted.id, positionSet.position
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertUserMessage(content string) edgedb.UUID {
|
func insertUserMessage(content string) edgedb.UUID {
|
||||||
|
9
main.go
9
main.go
@ -12,11 +12,13 @@ import (
|
|||||||
|
|
||||||
var userTmpl *pongo2.Template
|
var userTmpl *pongo2.Template
|
||||||
var botTmpl *pongo2.Template
|
var botTmpl *pongo2.Template
|
||||||
|
var modelSelecBtnTmpl *pongo2.Template
|
||||||
var sseChanel *ssefiber.FiberSSEChannel
|
var sseChanel *ssefiber.FiberSSEChannel
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
botTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-bot.html"))
|
botTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-bot.html"))
|
||||||
userTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-user.html"))
|
userTmpl = pongo2.Must(pongo2.FromFile("views/partials/message-user.html"))
|
||||||
|
modelSelecBtnTmpl = pongo2.Must(pongo2.FromFile("views/partials/model-selection-btn.html"))
|
||||||
|
|
||||||
// Import HTML using django engine/template
|
// Import HTML using django engine/template
|
||||||
engine := django.New("./views", ".html")
|
engine := django.New("./views", ".html")
|
||||||
@ -28,7 +30,7 @@ func main() {
|
|||||||
EnablePrintRoutes: true,
|
EnablePrintRoutes: true,
|
||||||
})
|
})
|
||||||
sse := ssefiber.New(app, "/sse")
|
sse := ssefiber.New(app, "/sse")
|
||||||
sseChanel = sse.CreateChannel("sse", "/sse")
|
sseChanel = sse.CreateChannel("sse", "")
|
||||||
|
|
||||||
// Add default logger
|
// Add default logger
|
||||||
app.Use(logger.New())
|
app.Use(logger.New())
|
||||||
@ -41,7 +43,6 @@ func main() {
|
|||||||
app.Get("/loadChat", LoadChatHandler)
|
app.Get("/loadChat", LoadChatHandler)
|
||||||
|
|
||||||
// Chat routes
|
// Chat routes
|
||||||
app.Post("/requestMultipleMessages", RequestMultipleMessagesHandler)
|
|
||||||
app.Post("/deleteMessage", DeleteMessageHandler)
|
app.Post("/deleteMessage", DeleteMessageHandler)
|
||||||
app.Get("/generateMultipleMessages", GenerateMultipleMessages)
|
app.Get("/generateMultipleMessages", GenerateMultipleMessages)
|
||||||
app.Get("/messageContent", GetMessageContentHandler)
|
app.Get("/messageContent", GetMessageContentHandler)
|
||||||
@ -66,6 +67,10 @@ func main() {
|
|||||||
|
|
||||||
app.Get("/test", func(c *fiber.Ctx) error {
|
app.Get("/test", func(c *fiber.Ctx) error {
|
||||||
fmt.Println("Hello from test")
|
fmt.Println("Hello from test")
|
||||||
|
go sseChanel.SendEvent(
|
||||||
|
"swapIcon-1",
|
||||||
|
`<img src="icons/groq.png" alt="User Image">`,
|
||||||
|
)
|
||||||
return c.SendString("")
|
return c.SendString("")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
BIN
static/My logo - background (1).png
Normal file
BIN
static/My logo - background (1).png
Normal file
Binary file not shown.
After Width: | Height: | Size: 168 KiB |
@ -47,10 +47,6 @@ html {
|
|||||||
--bulma-primary: #126d0f;
|
--bulma-primary: #126d0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-primary {
|
|
||||||
background-color: #1ebc19;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat input stuff */
|
/* Chat input stuff */
|
||||||
.chat-input-container {
|
.chat-input-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
20
utils.go
20
utils.go
@ -124,3 +124,23 @@ func removeDuplicate(s []string) []string {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChangeRoleBot2Assistant(messages []Message) []Message {
|
||||||
|
openaiMessages := make([]Message, len(messages))
|
||||||
|
for i, msg := range messages {
|
||||||
|
var role string
|
||||||
|
switch msg.Role {
|
||||||
|
case "user":
|
||||||
|
role = "user"
|
||||||
|
case "bot":
|
||||||
|
role = "assistant"
|
||||||
|
default:
|
||||||
|
role = "system"
|
||||||
|
}
|
||||||
|
openaiMessages[i] = Message{
|
||||||
|
Role: role,
|
||||||
|
Content: msg.Content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return openaiMessages
|
||||||
|
}
|
||||||
|
@ -18,8 +18,13 @@
|
|||||||
<i class="fa-solid fa-broom"></i>
|
<i class="fa-solid fa-broom"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="submit" class="button is-small" hx-get="/test" hx-swap="beforeend">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-vials"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button disabled type="submit" class="send-button button is-primary is-small"
|
<button disabled type="submit" class="send-button button is-primary is-small"
|
||||||
hx-post="/requestMultipleMessages" hx-swap="beforeend settle:200ms" hx-target="#chat-messages"
|
hx-get="/generateMultipleMessages" hx-swap="beforeend settle:200ms" hx-target="#chat-messages"
|
||||||
id="chat-input-send-btn" class="chat-input" hx-include="[name='message'], [name^='model-check-']"
|
id="chat-input-send-btn" class="chat-input" hx-include="[name='message'], [name^='model-check-']"
|
||||||
onclick="clearTextArea()">
|
onclick="clearTextArea()">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html data-theme="dark" lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -11,11 +11,12 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" href="/animations.css">
|
<link rel="stylesheet" href="/animations.css">
|
||||||
<link rel="stylesheet" href="/style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/sse.js"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/sse.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body hx-ext="sse" sse-connect="/sse">
|
||||||
|
|
||||||
{{embed}}
|
{{embed}}
|
||||||
|
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
<div class="message-bot mt-3">
|
<div class="message-bot mt-3">
|
||||||
<div class="columns is-mobile">
|
<div class="columns is-mobile">
|
||||||
<div class="column is-narrow" id="icon-column">
|
<div class="column is-narrow" id="icon-column">
|
||||||
|
<!-- Left column with the icon -->
|
||||||
|
{% if IsPlaceholder %}
|
||||||
|
|
||||||
|
<figure class="image is-48x48" style="flex-shrink: 0;" id="selectedIcon-{{ ConversationAreaId }}"
|
||||||
|
sse-swap="swapIcon-{{ ConversationAreaId }}">
|
||||||
|
<img src="icons/bouvai2.png" alt="User Image">
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
{% for message in Messages %}
|
{% for message in Messages %}
|
||||||
{% if not message.Hidden %}
|
{% if not message.Hidden %}
|
||||||
<figure class="image is-48x48" style="flex-shrink: 0;">
|
<figure class="image is-48x48" style="flex-shrink: 0;" id="selectedIcon-{{ ConversationAreaId }}"
|
||||||
<img id="selectedIcon-{{ ConversationAreaId }}" src="icons/{{ message.Icon }}.png" alt="User Image">
|
sse-swap="swapIcon-{{ ConversationAreaId }}">
|
||||||
|
<img src="icons/{{ message.Icon }}.png" alt="User Image">
|
||||||
</figure>
|
</figure>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if IsPlaceholder %}
|
|
||||||
<figure class="image is-48x48" style="flex-shrink: 0;">
|
|
||||||
<img src="icons/bouvai2.png" alt="User Image">
|
|
||||||
</figure>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column" id="content-column">
|
<div class="column" id="content-column">
|
||||||
{% if not IsPlaceholder %}
|
{% if not IsPlaceholder %}
|
||||||
<div class="is-flex is-align-items-start">
|
<div class="is-flex is-align-items-start">
|
||||||
<div class="message-content" id="content-{{ ConversationAreaId }}">
|
<div class="message-content" id="content-{{ ConversationAreaId }}"
|
||||||
|
sse-swap="swapContent-{{ ConversationAreaId }}">
|
||||||
{% for message in Messages %}
|
{% for message in Messages %}
|
||||||
{% if not message.Hidden %}
|
{% if not message.Hidden %}
|
||||||
<div class="message-header">
|
<div class="message-header">
|
||||||
@ -55,19 +64,32 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% elif IsPlaceholder %}
|
{% elif IsPlaceholder %}
|
||||||
<hx hx-get="/generateMultipleMessages" hx-trigger="load" hx-swap="outerHTML" hx-indicator="#spinner"
|
<div class="is-flex is-align-items-start">
|
||||||
hx-target="#chat-container">
|
<div class="message-content" id="content-{{ ConversationAreaId }}"
|
||||||
</hx>
|
sse-swap="swapContent-{{ ConversationAreaId }}">
|
||||||
|
|
||||||
<div class="message-content" {% if message.Hidden %}style="display: none;" {% endif %}>
|
|
||||||
<div class="message-body">
|
<div class="message-body">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<img id="spinner" class="htmx-indicator" src="/puff.svg" />
|
<img src="/puff.svg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="is-flex is-justify-content mt-2">
|
||||||
|
{% for selectedModel in selectedModels %}
|
||||||
|
<button disable class="button is-small is-primary message-button is-outlined mr-1"
|
||||||
|
sse-swap="swapSelectionBtn-{{ selectedModel.ID }}" hx-swap="outerHTML">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<!--img src="icons/{{ selectedModel.Icon }}.png" alt="{{ selectedModel.Name }}"
|
||||||
|
style="max-height: 100%; max-width: 100%;"-->
|
||||||
|
<img src="/puff.svg" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
7
views/partials/model-selection-btn.html
Normal file
7
views/partials/model-selection-btn.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<button class="button is-small is-primary message-button is-outlined mr-1" hx-get="/messageContent?id={{ message.Id }}"
|
||||||
|
hx-target="#content-{{ ConversationAreaId }}" onclick="updateIcon('{{ message.Icon }}', '{{ ConversationAreaId }}')"
|
||||||
|
title="{{ message.Name }}">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<img src="icons/{{ message.Icon }}.png" alt="{{ message.Name }}" style="max-height: 100%; max-width: 100%;">
|
||||||
|
</span>
|
||||||
|
</button>
|
Loading…
x
Reference in New Issue
Block a user