mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 08:04:21 +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:
parent
88daa841fd
commit
14c9bea663
13 changed files with 2337 additions and 94 deletions
|
@ -1075,11 +1075,12 @@
|
|||
threeDotMenu.appendChild(conversationMenu);
|
||||
|
||||
let deleteButton = document.createElement('button');
|
||||
deleteButton.type = "button";
|
||||
deleteButton.innerHTML = "Delete";
|
||||
deleteButton.classList.add("delete-conversation-button");
|
||||
deleteButton.classList.add("three-dot-menu-button-item");
|
||||
deleteButton.addEventListener('click', function() {
|
||||
// Ask for confirmation before deleting chat session
|
||||
deleteButton.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
let confirmation = confirm('Are you sure you want to delete this chat session?');
|
||||
if (!confirmation) return;
|
||||
let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`;
|
||||
|
@ -1927,6 +1928,7 @@
|
|||
text-align: left;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.three-dot-menu {
|
||||
|
|
|
@ -36,6 +36,7 @@ from khoj.database.models import (
|
|||
NotionConfig,
|
||||
OpenAIProcessorConversationConfig,
|
||||
ProcessLock,
|
||||
PublicConversation,
|
||||
ReflectiveQuestion,
|
||||
SearchModelConfig,
|
||||
SpeechToTextModelOptions,
|
||||
|
@ -560,7 +561,28 @@ class AgentAdapters:
|
|||
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:
|
||||
@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
|
||||
def get_conversation_by_user(
|
||||
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
|
||||
|
@ -680,6 +702,19 @@ class ConversationAdapters:
|
|||
async def aget_default_conversation_config():
|
||||
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
|
||||
def save_conversation(
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
question = models.CharField(max_length=500)
|
||||
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) {
|
||||
let menu = document.getElementById("khoj-nav-menu");
|
||||
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
||||
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
||||
if (isClickOnMenu === false && menu.classList.contains("show")) {
|
||||
menu.classList.remove("show");
|
||||
if (menuContainer) {
|
||||
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
||||
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);
|
||||
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');
|
||||
deleteButton.type = "button";
|
||||
deleteButton.innerHTML = "Delete";
|
||||
deleteButton.classList.add("delete-conversation-button");
|
||||
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
|
||||
let confirmation = confirm('Are you sure you want to delete this chat session?');
|
||||
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 {
|
||||
background: var(--background-color);
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
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;
|
||||
}
|
||||
|
||||
button#copy-share-url-button,
|
||||
button#new-conversation-button {
|
||||
display: inline-flex;
|
||||
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;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.three-dot-menu {
|
||||
display: block;
|
||||
/* background: var(--background-color); */
|
||||
/* border: 1px solid var(--main-text-color); */
|
||||
border-radius: 5px;
|
||||
/* position: relative; */
|
||||
position: absolute;
|
||||
right: 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;
|
||||
}
|
||||
|
||||
#agent-instructions {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
height: 50px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#agent-owned-by-user {
|
||||
font-size: 12px;
|
||||
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 */
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 250px;
|
||||
width: 300px;
|
||||
text-align: left;
|
||||
background: var(--background-color);
|
||||
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);
|
||||
}
|
||||
|
||||
.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 {
|
||||
background: var(--summer-sun);
|
||||
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;
|
||||
}
|
||||
|
||||
button#copy-share-url-button:hover,
|
||||
button#new-conversation-submit-button:hover {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Login Modal -->
|
||||
<div id="login-modal">
|
||||
<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 -->
|
||||
<div
|
||||
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 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.processor.conversation.prompts import (
|
||||
help_message,
|
||||
|
@ -132,6 +137,60 @@ def chat_history(
|
|||
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")
|
||||
@requires(["authenticated"])
|
||||
async def clear_chat_history(
|
||||
|
@ -154,6 +213,66 @@ async def clear_chat_history(
|
|||
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")
|
||||
@requires(["authenticated"])
|
||||
def chat_sessions(
|
||||
|
|
|
@ -14,6 +14,7 @@ from khoj.database.adapters import (
|
|||
AutomationAdapters,
|
||||
ConversationAdapters,
|
||||
EntryAdapters,
|
||||
PublicConversationAdapters,
|
||||
get_user_github_config,
|
||||
get_user_name,
|
||||
get_user_notion_config,
|
||||
|
@ -350,9 +351,9 @@ def notion_config_page(request: Request):
|
|||
@web_client.get("/config/content-source/computer", response_class=HTMLResponse)
|
||||
@requires(["authenticated"], redirect="login_page")
|
||||
def computer_config_page(request: Request):
|
||||
user = request.user.object
|
||||
user_picture = request.session.get("user", {}).get("picture")
|
||||
has_documents = EntryAdapters.user_has_entries(user=user)
|
||||
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
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"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)
|
||||
@requires(["authenticated"], redirect="login_page")
|
||||
def automations_config_page(request: Request):
|
||||
|
|
Loading…
Add table
Reference in a new issue