Skip to content

Commit

Permalink
Support multiple chat sessions within the web UI (#638)
Browse files Browse the repository at this point in the history
* Enable support for multiple chat sessions within the web client

- Allow users to create multiple chat sessions and manage them
- Give chat session slugs based on the most recent message
- Update web UI to have a collapsible menu with active chats
- Move chat routes into a separate file

* Make the collapsible side panel more graceful, improve some styling elements of the new layout

* Support modification of the conversation title

- Add a new field to the conversation object
- Update UI to add a threedotmenu to each conversation

* Get the default conversation if a matching one is not found by id
  • Loading branch information
sabaimran authored Feb 11, 2024
1 parent 208ccc8 commit 1412ed6
Show file tree
Hide file tree
Showing 15 changed files with 981 additions and 301 deletions.
4 changes: 2 additions & 2 deletions src/interface/desktop/assets/khoj.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ a.khoj-nav-selected {
background-color: var(--primary);
}
img.khoj-logo {
width: min(60vw, 111px);
width: min(60vw, 90px);
max-width: 100%;
justify-self: center;
}
Expand All @@ -117,7 +117,7 @@ img.khoj-logo {
display: grid;
grid-auto-flow: column;
gap: 20px;
padding: 16px 10px;
padding: 12px 10px;
margin: 0 0 16px 0;
}

Expand Down
6 changes: 3 additions & 3 deletions src/interface/desktop/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@

// Add event listener to toggle full reference on click
referenceButton.addEventListener('click', function() {
console.log(`Toggling ref-${index}`)
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
Expand Down Expand Up @@ -100,7 +99,6 @@

// Add event listener to toggle full reference on click
referenceButton.addEventListener('click', function() {
console.log(`Toggling ref-${index}`)
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
Expand Down Expand Up @@ -586,8 +584,10 @@
return data.response;
})
.then(response => {

const fullChatLog = response.chat;
// Render conversation history, if any
response.forEach(chat_log => {
fullChatLog.forEach(chat_log => {
renderMessageWithReference(chat_log.message, chat_log.by, chat_log.context, new Date(chat_log.created), chat_log.onlineContext, chat_log.intent?.type, chat_log.intent?.["inferred-queries"]);
});
})
Expand Down
2 changes: 1 addition & 1 deletion src/interface/obsidian/src/chat_modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export class KhojChatModal extends Modal {

return false;
} else if (responseJson.response) {
let chatLogs = responseJson.response;
let chatLogs = responseJson.response.chat;
chatLogs.forEach((chatLog: any) => {
this.renderMessageWithReferences(chatBodyEl, chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created), chatLog.intent?.type);
});
Expand Down
2 changes: 2 additions & 0 deletions src/khoj/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non
def configure_routes(app):
# Import APIs here to setup search types before while configuring server
from khoj.routers.api import api
from khoj.routers.api_chat import api_chat
from khoj.routers.api_config import api_config
from khoj.routers.auth import auth_router
from khoj.routers.indexer import indexer
Expand All @@ -266,6 +267,7 @@ def configure_routes(app):
app.include_router(indexer, prefix="/api/v1/index")
app.include_router(web_client)
app.include_router(auth_router, prefix="/auth")
app.include_router(api_chat, prefix="/api/chat")

if state.billing_enabled:
from khoj.routers.subscription import subscription_router
Expand Down
73 changes: 62 additions & 11 deletions src/khoj/database/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,22 +357,61 @@ async def aget_client_application_by_id(client_id: str, client_secret: str):

class ConversationAdapters:
@staticmethod
def get_conversation_by_user(user: KhojUser, client_application: ClientApplication = None):
conversation = Conversation.objects.filter(user=user, client=client_application)
def get_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
):
if conversation_id:
conversation = Conversation.objects.filter(user=user, client=client_application, id=conversation_id)
if not conversation_id or not conversation.exists():
conversation = Conversation.objects.filter(user=user, client=client_application)
if conversation.exists():
return conversation.first()
return Conversation.objects.create(user=user, client=client_application)

@staticmethod
async def aget_conversation_by_user(user: KhojUser, client_application: ClientApplication = None):
conversation = Conversation.objects.filter(user=user, client=client_application)
def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None):
return Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at")

@staticmethod
async def aset_conversation_title(
user: KhojUser, client_application: ClientApplication, conversation_id: int, title: str
):
conversation = await Conversation.objects.filter(
user=user, client=client_application, id=conversation_id
).afirst()
if conversation:
conversation.title = title
await conversation.asave()
return conversation
return None

@staticmethod
def get_conversation_by_id(conversation_id: int):
return Conversation.objects.filter(id=conversation_id).first()

@staticmethod
async def acreate_conversation_session(user: KhojUser, client_application: ClientApplication = None):
return await Conversation.objects.acreate(user=user, client=client_application)

@staticmethod
async def aget_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None, slug: str = None
):
if conversation_id:
conversation = Conversation.objects.filter(user=user, client=client_application, id=conversation_id)
else:
conversation = Conversation.objects.filter(user=user, client=client_application, slug=slug)
if await conversation.aexists():
return await conversation.afirst()
return await Conversation.objects.acreate(user=user, client=client_application)
return await Conversation.objects.acreate(user=user, client=client_application, slug=slug)

@staticmethod
async def adelete_conversation_by_user(user: KhojUser):
return await Conversation.objects.filter(user=user).adelete()
async def adelete_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
):
if conversation_id:
return await Conversation.objects.filter(user=user, client=client_application, id=conversation_id).adelete()
return await Conversation.objects.filter(user=user, client=client_application).adelete()

@staticmethod
def has_any_conversation_config(user: KhojUser):
Expand Down Expand Up @@ -433,12 +472,24 @@ async def aget_default_conversation_config():
return await ChatModelOptions.objects.filter().afirst()

@staticmethod
def save_conversation(user: KhojUser, conversation_log: dict, client_application: ClientApplication = None):
conversation = Conversation.objects.filter(user=user, client=client_application)
def save_conversation(
user: KhojUser,
conversation_log: dict,
client_application: ClientApplication = None,
conversation_id: int = None,
user_message: str = None,
):
slug = user_message.strip()[:200] if not is_none_or_empty(user_message) else None
if conversation_id:
conversation = Conversation.objects.filter(user=user, client=client_application, id=conversation_id)
else:
conversation = Conversation.objects.filter(user=user, client=client_application)
if conversation.exists():
conversation.update(conversation_log=conversation_log)
conversation.update(conversation_log=conversation_log, slug=slug)
else:
Conversation.objects.create(user=user, conversation_log=conversation_log, client=client_application)
Conversation.objects.create(
user=user, conversation_log=conversation_log, client=client_application, slug=slug
)

@staticmethod
def get_conversation_processor_options():
Expand Down
22 changes: 22 additions & 0 deletions src/khoj/database/migrations/0030_conversation_slug_and_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.7 on 2024-02-05 04:39

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("database", "0029_userrequests"),
]

operations = [
migrations.AddField(
model_name="conversation",
name="slug",
field=models.CharField(blank=True, default=None, max_length=200, null=True),
),
migrations.AddField(
model_name="conversation",
name="title",
field=models.CharField(blank=True, default=None, max_length=200, null=True),
),
]
2 changes: 2 additions & 0 deletions src/khoj/database/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ class Conversation(BaseModel):
user = models.ForeignKey(KhojUser, on_delete=models.CASCADE)
conversation_log = models.JSONField(default=dict)
client = models.ForeignKey(ClientApplication, on_delete=models.CASCADE, default=None, null=True, blank=True)
slug = models.CharField(max_length=200, default=None, null=True, blank=True)
title = models.CharField(max_length=200, default=None, null=True, blank=True)


class ReflectiveQuestion(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions src/khoj/interface/web/assets/khoj.css
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ a.khoj-logo {
background-color: var(--primary);
}
img.khoj-logo {
width: min(60vw, 111px);
width: min(60vw, 90px);
max-width: 100%;
justify-self: center;
}
Expand Down Expand Up @@ -202,7 +202,7 @@ img.khoj-logo {
grid-auto-flow: column;
gap: 20px;
padding: 16px 10px;
margin: 0 0 16px 0;
margin: 0;
}

nav.khoj-nav {
Expand Down
Loading

0 comments on commit 1412ed6

Please sign in to comment.