mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-06 05:39:12 +00:00
Make conversations optionally shareable (#712)
* Make conversations optionally shareable - Shared conversations are viewable by anyone, without a login wall - Can share a conversation from the three dot menu - Add a new model for Public Conversation - The rationale for a separate model is that public and private conversations have different assumptions. Separating them reduces some of the code specificity on our server-side code and allows us for easier interpretation and stricter security. Separating the data model makes it harder to accidentally view something that was meant to be private - Add a new, read-only view for public conversations
This commit is contained in:
@@ -1075,11 +1075,12 @@
|
|||||||
threeDotMenu.appendChild(conversationMenu);
|
threeDotMenu.appendChild(conversationMenu);
|
||||||
|
|
||||||
let deleteButton = document.createElement('button');
|
let deleteButton = document.createElement('button');
|
||||||
|
deleteButton.type = "button";
|
||||||
deleteButton.innerHTML = "Delete";
|
deleteButton.innerHTML = "Delete";
|
||||||
deleteButton.classList.add("delete-conversation-button");
|
deleteButton.classList.add("delete-conversation-button");
|
||||||
deleteButton.classList.add("three-dot-menu-button-item");
|
deleteButton.classList.add("three-dot-menu-button-item");
|
||||||
deleteButton.addEventListener('click', function() {
|
deleteButton.addEventListener('click', function(event) {
|
||||||
// Ask for confirmation before deleting chat session
|
event.preventDefault();
|
||||||
let confirmation = confirm('Are you sure you want to delete this chat session?');
|
let confirmation = confirm('Are you sure you want to delete this chat session?');
|
||||||
if (!confirmation) return;
|
if (!confirmation) return;
|
||||||
let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`;
|
let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`;
|
||||||
@@ -1927,6 +1928,7 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.three-dot-menu {
|
.three-dot-menu {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ from khoj.database.models import (
|
|||||||
NotionConfig,
|
NotionConfig,
|
||||||
OpenAIProcessorConversationConfig,
|
OpenAIProcessorConversationConfig,
|
||||||
ProcessLock,
|
ProcessLock,
|
||||||
|
PublicConversation,
|
||||||
ReflectiveQuestion,
|
ReflectiveQuestion,
|
||||||
SearchModelConfig,
|
SearchModelConfig,
|
||||||
SpeechToTextModelOptions,
|
SpeechToTextModelOptions,
|
||||||
@@ -560,7 +561,28 @@ class AgentAdapters:
|
|||||||
return await Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).afirst()
|
return await Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).afirst()
|
||||||
|
|
||||||
|
|
||||||
|
class PublicConversationAdapters:
|
||||||
|
@staticmethod
|
||||||
|
def get_public_conversation_by_slug(slug: str):
|
||||||
|
return PublicConversation.objects.filter(slug=slug).first()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_public_conversation_url(public_conversation: PublicConversation):
|
||||||
|
# Public conversations are viewable by anyone, but not editable.
|
||||||
|
return f"/share/chat/{public_conversation.slug}/"
|
||||||
|
|
||||||
|
|
||||||
class ConversationAdapters:
|
class ConversationAdapters:
|
||||||
|
@staticmethod
|
||||||
|
def make_public_conversation_copy(conversation: Conversation):
|
||||||
|
return PublicConversation.objects.create(
|
||||||
|
source_owner=conversation.user,
|
||||||
|
agent=conversation.agent,
|
||||||
|
conversation_log=conversation.conversation_log,
|
||||||
|
slug=conversation.slug,
|
||||||
|
title=conversation.title,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_conversation_by_user(
|
def get_conversation_by_user(
|
||||||
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
|
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
|
||||||
@@ -680,6 +702,19 @@ class ConversationAdapters:
|
|||||||
async def aget_default_conversation_config():
|
async def aget_default_conversation_config():
|
||||||
return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst()
|
return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_conversation_from_public_conversation(
|
||||||
|
user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication
|
||||||
|
):
|
||||||
|
return Conversation.objects.create(
|
||||||
|
user=user,
|
||||||
|
conversation_log=public_conversation.conversation_log,
|
||||||
|
client=client_app,
|
||||||
|
slug=public_conversation.slug,
|
||||||
|
title=public_conversation.title,
|
||||||
|
agent=public_conversation.agent,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def save_conversation(
|
def save_conversation(
|
||||||
user: KhojUser,
|
user: KhojUser,
|
||||||
|
|||||||
42
src/khoj/database/migrations/0036_publicconversation.py
Normal file
42
src/khoj/database/migrations/0036_publicconversation.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 4.2.10 on 2024-04-17 13:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("database", "0035_processlock"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PublicConversation",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("conversation_log", models.JSONField(default=dict)),
|
||||||
|
("slug", models.CharField(blank=True, default=None, max_length=200, null=True)),
|
||||||
|
("title", models.CharField(blank=True, default=None, max_length=200, null=True)),
|
||||||
|
(
|
||||||
|
"agent",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="database.agent",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"source_owner",
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
14
src/khoj/database/migrations/0040_merge_20240504_1010.py
Normal file
14
src/khoj/database/migrations/0040_merge_20240504_1010.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 4.2.10 on 2024-05-04 10:10
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("database", "0036_publicconversation"),
|
||||||
|
("database", "0039_merge_20240501_0301"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations: List[str] = []
|
||||||
14
src/khoj/database/migrations/0041_merge_20240505_1234.py
Normal file
14
src/khoj/database/migrations/0041_merge_20240505_1234.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Generated by Django 4.2.10 on 2024-05-05 12:34
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("database", "0040_alter_processlock_name"),
|
||||||
|
("database", "0040_merge_20240504_1010"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations: List[str] = []
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from random import choice
|
from random import choice
|
||||||
|
|
||||||
@@ -249,6 +250,36 @@ class Conversation(BaseModel):
|
|||||||
agent = models.ForeignKey(Agent, on_delete=models.SET_NULL, default=None, null=True, blank=True)
|
agent = models.ForeignKey(Agent, on_delete=models.SET_NULL, default=None, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicConversation(BaseModel):
|
||||||
|
source_owner = models.ForeignKey(KhojUser, on_delete=models.CASCADE)
|
||||||
|
conversation_log = models.JSONField(default=dict)
|
||||||
|
slug = models.CharField(max_length=200, default=None, null=True, blank=True)
|
||||||
|
title = models.CharField(max_length=200, default=None, null=True, blank=True)
|
||||||
|
agent = models.ForeignKey(Agent, on_delete=models.SET_NULL, default=None, null=True, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=PublicConversation)
|
||||||
|
def verify_public_conversation(sender, instance, **kwargs):
|
||||||
|
def generate_random_alphanumeric(length):
|
||||||
|
characters = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||||
|
return "".join(choice(characters) for _ in range(length))
|
||||||
|
|
||||||
|
# check if this is a new instance
|
||||||
|
if instance._state.adding:
|
||||||
|
slug = re.sub(r"\W+", "-", instance.slug.lower())[:50]
|
||||||
|
observed_random_id = set()
|
||||||
|
while PublicConversation.objects.filter(slug=slug).exists():
|
||||||
|
try:
|
||||||
|
random_id = generate_random_alphanumeric(7)
|
||||||
|
except IndexError:
|
||||||
|
raise ValidationError(
|
||||||
|
"Unable to generate a unique slug for the Public Conversation. Please try again later."
|
||||||
|
)
|
||||||
|
observed_random_id.add(random_id)
|
||||||
|
slug = f"{slug}-{random_id}"
|
||||||
|
instance.slug = slug
|
||||||
|
|
||||||
|
|
||||||
class ReflectiveQuestion(BaseModel):
|
class ReflectiveQuestion(BaseModel):
|
||||||
question = models.CharField(max_length=500)
|
question = models.CharField(max_length=500)
|
||||||
user = models.ForeignKey(KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True)
|
user = models.ForeignKey(KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True)
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ function toggleMenu() {
|
|||||||
document.addEventListener('click', function(event) {
|
document.addEventListener('click', function(event) {
|
||||||
let menu = document.getElementById("khoj-nav-menu");
|
let menu = document.getElementById("khoj-nav-menu");
|
||||||
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
||||||
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
if (menuContainer) {
|
||||||
if (isClickOnMenu === false && menu.classList.contains("show")) {
|
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
||||||
menu.classList.remove("show");
|
if (isClickOnMenu === false && menu.classList.contains("show")) {
|
||||||
|
menu.classList.remove("show");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1562,11 +1562,79 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
conversationMenu.appendChild(editTitleButton);
|
conversationMenu.appendChild(editTitleButton);
|
||||||
threeDotMenu.appendChild(conversationMenu);
|
threeDotMenu.appendChild(conversationMenu);
|
||||||
|
|
||||||
|
let shareButton = document.createElement('button');
|
||||||
|
shareButton.innerHTML = "Share";
|
||||||
|
shareButton.type = "button";
|
||||||
|
shareButton.classList.add("share-conversation-button");
|
||||||
|
shareButton.classList.add("three-dot-menu-button-item");
|
||||||
|
shareButton.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let confirmation = confirm('Are you sure you want to share this chat session? This will make the conversation public.');
|
||||||
|
if (!confirmation) return;
|
||||||
|
let duplicateURL = `/api/chat/share?client=web&conversation_id=${incomingConversationId}`;
|
||||||
|
fetch(duplicateURL , { method: "POST" })
|
||||||
|
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||||
|
.then(data => {
|
||||||
|
if (data.status == "ok") {
|
||||||
|
flashStatusInChatInput("✅ Conversation shared successfully");
|
||||||
|
}
|
||||||
|
// Make a pop-up that shows data.url to share the conversation
|
||||||
|
let shareURL = data.url;
|
||||||
|
let shareModal = document.createElement('div');
|
||||||
|
shareModal.classList.add("modal");
|
||||||
|
shareModal.id = "share-conversation-modal";
|
||||||
|
let shareModalContent = document.createElement('div');
|
||||||
|
shareModalContent.classList.add("modal-content");
|
||||||
|
let shareModalHeader = document.createElement('div');
|
||||||
|
shareModalHeader.classList.add("modal-header");
|
||||||
|
let shareModalTitle = document.createElement('h2');
|
||||||
|
shareModalTitle.textContent = "Share Conversation";
|
||||||
|
let shareModalCloseButton = document.createElement('button');
|
||||||
|
shareModalCloseButton.classList.add("modal-close-button");
|
||||||
|
shareModalCloseButton.innerHTML = "×";
|
||||||
|
shareModalCloseButton.addEventListener('click', function() {
|
||||||
|
shareModal.remove();
|
||||||
|
});
|
||||||
|
shareModalHeader.appendChild(shareModalTitle);
|
||||||
|
shareModalHeader.appendChild(shareModalCloseButton);
|
||||||
|
shareModalContent.appendChild(shareModalHeader);
|
||||||
|
let shareModalBody = document.createElement('div');
|
||||||
|
shareModalBody.classList.add("modal-body");
|
||||||
|
let shareModalText = document.createElement('p');
|
||||||
|
shareModalText.textContent = "The link has been copied to your clipboard. Use it to share your conversation with others!";
|
||||||
|
let shareModalLink = document.createElement('input');
|
||||||
|
shareModalLink.setAttribute("value", shareURL);
|
||||||
|
shareModalLink.setAttribute("readonly", "");
|
||||||
|
shareModalLink.classList.add("share-link");
|
||||||
|
let copyButton = document.createElement('button');
|
||||||
|
copyButton.textContent = "Copy";
|
||||||
|
copyButton.addEventListener('click', function() {
|
||||||
|
shareModalLink.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
});
|
||||||
|
copyButton.id = "copy-share-url-button";
|
||||||
|
shareModalBody.appendChild(shareModalText);
|
||||||
|
shareModalBody.appendChild(shareModalLink);
|
||||||
|
shareModalBody.appendChild(copyButton);
|
||||||
|
shareModalContent.appendChild(shareModalBody);
|
||||||
|
shareModal.appendChild(shareModalContent);
|
||||||
|
document.body.appendChild(shareModal);
|
||||||
|
shareModalLink.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
conversationMenu.appendChild(shareButton);
|
||||||
|
|
||||||
let deleteButton = document.createElement('button');
|
let deleteButton = document.createElement('button');
|
||||||
|
deleteButton.type = "button";
|
||||||
deleteButton.innerHTML = "Delete";
|
deleteButton.innerHTML = "Delete";
|
||||||
deleteButton.classList.add("delete-conversation-button");
|
deleteButton.classList.add("delete-conversation-button");
|
||||||
deleteButton.classList.add("three-dot-menu-button-item");
|
deleteButton.classList.add("three-dot-menu-button-item");
|
||||||
deleteButton.addEventListener('click', function() {
|
deleteButton.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
// Ask for confirmation before deleting chat session
|
// Ask for confirmation before deleting chat session
|
||||||
let confirmation = confirm('Are you sure you want to delete this chat session?');
|
let confirmation = confirm('Are you sure you want to delete this chat session?');
|
||||||
if (!confirmation) return;
|
if (!confirmation) return;
|
||||||
@@ -2225,7 +2293,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-panel-button {
|
.side-panel-button {
|
||||||
background: var(--background-color);
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -2444,6 +2512,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button#copy-share-url-button,
|
||||||
button#new-conversation-button {
|
button#new-conversation-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2464,14 +2533,12 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.three-dot-menu {
|
.three-dot-menu {
|
||||||
display: block;
|
display: block;
|
||||||
/* background: var(--background-color); */
|
|
||||||
/* border: 1px solid var(--main-text-color); */
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
/* position: relative; */
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
@@ -2653,13 +2720,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
#agent-instructions {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
height: 50px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#agent-owned-by-user {
|
#agent-owned-by-user {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #007BFF;
|
color: #007BFF;
|
||||||
@@ -2681,7 +2741,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
margin: 15% auto; /* 15% from the top and centered */
|
margin: 15% auto; /* 15% from the top and centered */
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #888;
|
border: 1px solid #888;
|
||||||
width: 250px;
|
width: 300px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -2755,6 +2815,28 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
border: 1px solid var(--main-text-color);
|
border: 1px solid var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.share-link {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-link:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007BFF;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
button#copy-share-url-button,
|
||||||
button#new-conversation-submit-button {
|
button#new-conversation-submit-button {
|
||||||
background: var(--summer-sun);
|
background: var(--summer-sun);
|
||||||
transition: background 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out;
|
||||||
@@ -2765,6 +2847,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||||||
transition: background 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button#copy-share-url-button:hover,
|
||||||
button#new-conversation-submit-button:hover {
|
button#new-conversation-submit-button:hover {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- Login Modal -->
|
<!-- Login Modal -->
|
||||||
<div id="login-modal">
|
<div id="login-modal">
|
||||||
<img class="khoj-logo" src="/static/assets/icons/favicon-128x128.png" alt="Khoj"></img>
|
<img class="khoj-logo" src="/static/assets/icons/favicon-128x128.png" alt="Khoj"></img>
|
||||||
<div class="login-modal-title">Log in to Khoj</div>
|
<div class="login-modal-title">Login to Khoj</div>
|
||||||
<!-- Sign Up/Login with Google OAuth -->
|
<!-- Sign Up/Login with Google OAuth -->
|
||||||
<div
|
<div
|
||||||
class="g_id_signin"
|
class="g_id_signin"
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
{% extends "base_config.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="page">
|
|
||||||
<div class="section">
|
|
||||||
<h2 class="section-title">
|
|
||||||
<img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat">
|
|
||||||
<span class="card-title-text">Chat</span>
|
|
||||||
</h2>
|
|
||||||
<form id="config-form">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<label for="openai-api-key" title="Get your OpenAI key from https://platform.openai.com/account/api-keys">OpenAI API key</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="openai-api-key" name="openai-api-key" value="{{ current_config['api_key'] }}">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<label for="chat-model">Chat Model</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input type="text" id="chat-model" name="chat-model" value="{{ current_config['chat_model'] }}">
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div class="section">
|
|
||||||
<div id="success" style="display: none;" ></div>
|
|
||||||
<button id="submit" type="submit">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
submit.addEventListener("click", function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
var openai_api_key = document.getElementById("openai-api-key").value;
|
|
||||||
var chat_model = document.getElementById("chat-model").value;
|
|
||||||
|
|
||||||
if (openai_api_key == "" || chat_model == "") {
|
|
||||||
document.getElementById("success").innerHTML = "⚠️ Please fill all the fields.";
|
|
||||||
document.getElementById("success").style.display = "block";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
|
|
||||||
fetch('/api/config/data/processor/conversation/openai', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
"api_key": openai_api_key,
|
|
||||||
"chat_model": chat_model
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data["status"] == "ok") {
|
|
||||||
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to your <a href='/config'>settings page</a> to complete setup.";
|
|
||||||
document.getElementById("success").style.display = "block";
|
|
||||||
} else {
|
|
||||||
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";
|
|
||||||
document.getElementById("success").style.display = "block";
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
1918
src/khoj/interface/web/public_conversation.html
Normal file
1918
src/khoj/interface/web/public_conversation.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,12 @@ from starlette.authentication import requires
|
|||||||
from starlette.websockets import WebSocketDisconnect
|
from starlette.websockets import WebSocketDisconnect
|
||||||
from websockets import ConnectionClosedOK
|
from websockets import ConnectionClosedOK
|
||||||
|
|
||||||
from khoj.database.adapters import ConversationAdapters, EntryAdapters, aget_user_name
|
from khoj.database.adapters import (
|
||||||
|
ConversationAdapters,
|
||||||
|
EntryAdapters,
|
||||||
|
PublicConversationAdapters,
|
||||||
|
aget_user_name,
|
||||||
|
)
|
||||||
from khoj.database.models import KhojUser
|
from khoj.database.models import KhojUser
|
||||||
from khoj.processor.conversation.prompts import (
|
from khoj.processor.conversation.prompts import (
|
||||||
help_message,
|
help_message,
|
||||||
@@ -132,6 +137,60 @@ def chat_history(
|
|||||||
return {"status": "ok", "response": meta_log}
|
return {"status": "ok", "response": meta_log}
|
||||||
|
|
||||||
|
|
||||||
|
@api_chat.get("/share/history")
|
||||||
|
def get_shared_chat(
|
||||||
|
request: Request,
|
||||||
|
common: CommonQueryParams,
|
||||||
|
public_conversation_slug: str,
|
||||||
|
n: Optional[int] = None,
|
||||||
|
):
|
||||||
|
user = request.user.object if request.user.is_authenticated else None
|
||||||
|
|
||||||
|
# Load Conversation History
|
||||||
|
conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
||||||
|
|
||||||
|
if conversation is None:
|
||||||
|
return Response(
|
||||||
|
content=json.dumps({"status": "error", "message": f"Conversation: {public_conversation_slug} not found"}),
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
agent_metadata = None
|
||||||
|
if conversation.agent:
|
||||||
|
agent_metadata = {
|
||||||
|
"slug": conversation.agent.slug,
|
||||||
|
"name": conversation.agent.name,
|
||||||
|
"avatar": conversation.agent.avatar,
|
||||||
|
"isCreator": conversation.agent.creator == user,
|
||||||
|
}
|
||||||
|
|
||||||
|
meta_log = conversation.conversation_log
|
||||||
|
meta_log.update(
|
||||||
|
{
|
||||||
|
"conversation_id": conversation.id,
|
||||||
|
"slug": conversation.title if conversation.title else conversation.slug,
|
||||||
|
"agent": agent_metadata,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if n:
|
||||||
|
# Get latest N messages if N > 0
|
||||||
|
if n > 0 and meta_log.get("chat"):
|
||||||
|
meta_log["chat"] = meta_log["chat"][-n:]
|
||||||
|
# Else return all messages except latest N
|
||||||
|
elif n < 0 and meta_log.get("chat"):
|
||||||
|
meta_log["chat"] = meta_log["chat"][:n]
|
||||||
|
|
||||||
|
update_telemetry_state(
|
||||||
|
request=request,
|
||||||
|
telemetry_type="api",
|
||||||
|
api="public_conversation_history",
|
||||||
|
**common.__dict__,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": "ok", "response": meta_log}
|
||||||
|
|
||||||
|
|
||||||
@api_chat.delete("/history")
|
@api_chat.delete("/history")
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
async def clear_chat_history(
|
async def clear_chat_history(
|
||||||
@@ -154,6 +213,66 @@ async def clear_chat_history(
|
|||||||
return {"status": "ok", "message": "Conversation history cleared"}
|
return {"status": "ok", "message": "Conversation history cleared"}
|
||||||
|
|
||||||
|
|
||||||
|
@api_chat.post("/share/fork")
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def fork_public_conversation(
|
||||||
|
request: Request,
|
||||||
|
common: CommonQueryParams,
|
||||||
|
public_conversation_slug: str,
|
||||||
|
):
|
||||||
|
user = request.user.object
|
||||||
|
|
||||||
|
# Load Conversation History
|
||||||
|
public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
||||||
|
|
||||||
|
# Duplicate Public Conversation to User's Private Conversation
|
||||||
|
ConversationAdapters.create_conversation_from_public_conversation(
|
||||||
|
user, public_conversation, request.user.client_app
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_metadata = {"forked_conversation": public_conversation.slug}
|
||||||
|
|
||||||
|
update_telemetry_state(
|
||||||
|
request=request,
|
||||||
|
telemetry_type="api",
|
||||||
|
api="fork_public_conversation",
|
||||||
|
**common.__dict__,
|
||||||
|
metadata=chat_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
redirect_uri = str(request.app.url_path_for("chat_page"))
|
||||||
|
|
||||||
|
return Response(status_code=200, content=json.dumps({"status": "ok", "next_url": redirect_uri}))
|
||||||
|
|
||||||
|
|
||||||
|
@api_chat.post("/share")
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def duplicate_chat_history_public_conversation(
|
||||||
|
request: Request,
|
||||||
|
common: CommonQueryParams,
|
||||||
|
conversation_id: int,
|
||||||
|
):
|
||||||
|
user = request.user.object
|
||||||
|
|
||||||
|
# Duplicate Conversation History to Public Conversation
|
||||||
|
conversation = ConversationAdapters.get_conversation_by_user(user, request.user.client_app, conversation_id)
|
||||||
|
|
||||||
|
public_conversation = ConversationAdapters.make_public_conversation_copy(conversation)
|
||||||
|
|
||||||
|
public_conversation_url = PublicConversationAdapters.get_public_conversation_url(public_conversation)
|
||||||
|
|
||||||
|
update_telemetry_state(
|
||||||
|
request=request,
|
||||||
|
telemetry_type="api",
|
||||||
|
api="post_chat_share",
|
||||||
|
**common.__dict__,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status_code=200, content=json.dumps({"status": "ok", "url": f"{request.client.host}{public_conversation_url}"})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@api_chat.get("/sessions")
|
@api_chat.get("/sessions")
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
def chat_sessions(
|
def chat_sessions(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from khoj.database.adapters import (
|
|||||||
AutomationAdapters,
|
AutomationAdapters,
|
||||||
ConversationAdapters,
|
ConversationAdapters,
|
||||||
EntryAdapters,
|
EntryAdapters,
|
||||||
|
PublicConversationAdapters,
|
||||||
get_user_github_config,
|
get_user_github_config,
|
||||||
get_user_name,
|
get_user_name,
|
||||||
get_user_notion_config,
|
get_user_notion_config,
|
||||||
@@ -350,9 +351,9 @@ def notion_config_page(request: Request):
|
|||||||
@web_client.get("/config/content-source/computer", response_class=HTMLResponse)
|
@web_client.get("/config/content-source/computer", response_class=HTMLResponse)
|
||||||
@requires(["authenticated"], redirect="login_page")
|
@requires(["authenticated"], redirect="login_page")
|
||||||
def computer_config_page(request: Request):
|
def computer_config_page(request: Request):
|
||||||
user = request.user.object
|
user = request.user.object if request.user.is_authenticated else None
|
||||||
user_picture = request.session.get("user", {}).get("picture")
|
user_picture = request.session.get("user", {}).get("picture") if user else None
|
||||||
has_documents = EntryAdapters.user_has_entries(user=user)
|
has_documents = EntryAdapters.user_has_entries(user=user) if user else False
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"content_source_computer_input.html",
|
"content_source_computer_input.html",
|
||||||
@@ -367,6 +368,59 @@ def computer_config_page(request: Request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@web_client.get("/share/chat/{public_conversation_slug}", response_class=HTMLResponse)
|
||||||
|
def view_public_conversation(request: Request):
|
||||||
|
public_conversation_slug = request.path_params.get("public_conversation_slug")
|
||||||
|
public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
||||||
|
if not public_conversation:
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"404.html",
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"khoj_version": state.khoj_version,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
user = request.user.object if request.user.is_authenticated else None
|
||||||
|
user_picture = request.session.get("user", {}).get("picture") if user else None
|
||||||
|
has_documents = EntryAdapters.user_has_entries(user=user) if user else False
|
||||||
|
|
||||||
|
all_agents = AgentAdapters.get_all_accessible_agents(request.user.object if request.user.is_authenticated else None)
|
||||||
|
|
||||||
|
# Filter out the current agent
|
||||||
|
all_agents = [agent for agent in all_agents if agent != public_conversation.agent]
|
||||||
|
agents_packet = []
|
||||||
|
for agent in all_agents:
|
||||||
|
agents_packet.append(
|
||||||
|
{
|
||||||
|
"slug": agent.slug,
|
||||||
|
"avatar": agent.avatar,
|
||||||
|
"name": agent.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
|
||||||
|
redirect_uri = str(request.app.url_path_for("auth"))
|
||||||
|
next_url = str(
|
||||||
|
request.app.url_path_for("view_public_conversation", public_conversation_slug=public_conversation_slug)
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"public_conversation.html",
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"username": user.username if user else None,
|
||||||
|
"user_photo": user_picture,
|
||||||
|
"is_active": has_required_scope(request, ["premium"]),
|
||||||
|
"has_documents": has_documents,
|
||||||
|
"khoj_version": state.khoj_version,
|
||||||
|
"public_conversation_slug": public_conversation_slug,
|
||||||
|
"agents": agents_packet,
|
||||||
|
"google_client_id": google_client_id,
|
||||||
|
"redirect_uri": f"{redirect_uri}?next={next_url}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@web_client.get("/automations", response_class=HTMLResponse)
|
@web_client.get("/automations", response_class=HTMLResponse)
|
||||||
@requires(["authenticated"], redirect="login_page")
|
@requires(["authenticated"], redirect="login_page")
|
||||||
def automations_config_page(request: Request):
|
def automations_config_page(request: Request):
|
||||||
|
|||||||
Reference in New Issue
Block a user