Skip to content

Commit

Permalink
Convert the default conversation id to a uuid, plus other fixes (#918)
Browse files Browse the repository at this point in the history
* Update the conversation_id primary key field to be a uuid

- update associated API endpoints
- this is to improve the overall application health, by obfuscating some information about the internal database
- conversation_id type is now implicitly a string, rather than an int
- ensure automations are also migrated in place, such that the conversation_ids they're pointing to are now mapped to the new IDs

* Update client-side API calls to correctly query with a string field

* Allow modifying of conversation properties from the chat title

* Improve drag and drop file experience for chat input area

* Use a phosphor icon for the copy to clipboard experience for code snippets

* Update conversation_id parameter to be a str type

* If django_apscheduler is not in the environment, skip the migration script

* Fix create automation flow by storing conversation id as string

The new UUID used for conversation id can't be directly serialized.
Convert to string for serializing it for later execution

---------

Co-authored-by: Debanjum Singh Solanky <[email protected]>
  • Loading branch information
sabaimran and debanjum committed Sep 24, 2024
1 parent 0c936ce commit 06777e1
Show file tree
Hide file tree
Showing 19 changed files with 213 additions and 66 deletions.
2 changes: 1 addition & 1 deletion src/interface/desktop/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
const chatApi = `${hostURL}/api/chat?client=desktop`;
const chatApiBody = {
q: query,
conversation_id: parseInt(conversationID),
conversation_id: conversationID,
stream: true,
...(!!city && { city: city }),
...(!!region && { region: region }),
Expand Down
2 changes: 1 addition & 1 deletion src/interface/desktop/shortcut.html
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@
const chatApi = `${hostURL}/api/chat?client=desktop`;
const chatApiBody = {
q: query,
conversation_id: parseInt(conversationID),
conversation_id: conversationID,
stream: true,
...(!!city && { city: city }),
...(!!region && { region: region }),
Expand Down
2 changes: 1 addition & 1 deletion src/interface/obsidian/src/chat_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1055,7 +1055,7 @@ export class KhojChatView extends KhojPaneView {
q: query,
n: this.setting.resultsCount,
stream: true,
...(!!conversationId && { conversation_id: parseInt(conversationId) }),
...(!!conversationId && { conversation_id: conversationId }),
...(!!this.location && {
city: this.location.city,
region: this.location.region,
Expand Down
5 changes: 4 additions & 1 deletion src/interface/web/app/chat/chat.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ input.inputBox:focus {
}

div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}

div.chatBodyFull {
Expand Down Expand Up @@ -94,6 +94,9 @@ div.agentIndicator {
padding: 10px;
}

div.chatTitleWrapper {
grid-template-columns: auto 1fr;
}

@media screen and (max-width: 768px) {
div.inputBox {
Expand Down
20 changes: 14 additions & 6 deletions src/interface/web/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import styles from "./chat.module.css";
import React, { Suspense, useEffect, useState } from "react";

import SidePanel from "../components/sidePanel/chatHistorySidePanel";
import SidePanel, { ChatSessionActionMenu } from "../components/sidePanel/chatHistorySidePanel";
import ChatHistory from "../components/chatHistory/chatHistory";
import { useSearchParams } from "next/navigation";
import Loading from "../components/loading/loading";
Expand All @@ -17,6 +17,8 @@ import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../common/u
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
import { useAuthenticatedData } from "../common/auth";
import { AgentData } from "../agents/page";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";

interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null;
Expand Down Expand Up @@ -104,7 +106,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
/>
</div>
<div
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl`}
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl h-fit`}
>
<ChatInputArea
agentColor={agentMetadata?.color}
Expand Down Expand Up @@ -133,6 +135,7 @@ export default function Chat() {
const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [image64, setImage64] = useState<string>("");

const locationData = useIPLocationData();
const authenticatedData = useAuthenticatedData();
const isMobileWidth = useIsMobileWidth();
Expand Down Expand Up @@ -235,7 +238,7 @@ export default function Chat() {
const chatAPI = "/api/chat?client=web";
const chatAPIBody = {
q: queryToProcess,
conversation_id: parseInt(conversationId),
conversation_id: conversationId,
stream: true,
...(locationData && {
region: locationData.region,
Expand Down Expand Up @@ -297,17 +300,22 @@ export default function Chat() {
</div>
<div className={styles.chatBox}>
<div className={styles.chatBoxBody}>
{!isMobileWidth && (
{!isMobileWidth && conversationId && (
<div
className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}
className={`${styles.chatTitleWrapper} text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8 pt-6 col-auto h-fit`}
>
{title && (
<h2
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden`}
>
{title}
</h2>
)}
<ChatSessionActionMenu
conversationId={conversationId}
setTitle={setTitle}
sizing="md"
/>
</div>
)}
<Suspense fallback={<Loading />}>
Expand Down
1 change: 0 additions & 1 deletion src/interface/web/app/common/chatFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ export function modifyFileFilterForConversation(
})
.then((response) => response.json())
.then((data) => {
console.log("ADDEDFILES DATA: ", data);
setAddedFiles(data);
})
.catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
let conversationFetchURL = "";

if (props.conversationId) {
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`;
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${encodeURIComponent(props.conversationId)}&n=${10 * nextPage}`;
} else if (props.publicConversationSlug) {
conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ export default function ChatInputArea(props: ChatInputProps) {
</div>
)}
<div
className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700 relative`}
className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700 relative ${isDragAndDropping && "animate-pulse"}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDragAndDropFiles}
Expand Down Expand Up @@ -547,7 +547,6 @@ export default function ChatInputArea(props: ChatInputProps) {
<ArrowUp className="w-6 h-6" weight="bold" />
</Button>
</div>
{isDragAndDropping && <div className="text-muted-foreground">Drop file to upload</div>}
</>
);
}
12 changes: 5 additions & 7 deletions src/interface/web/app/components/chatMessage/chatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styles from "./chatMessage.module.css";
import markdownIt from "markdown-it";
import mditHljs from "markdown-it-highlightjs";
import React, { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";

import "katex/dist/katex.min.css";

Expand All @@ -23,6 +24,7 @@ import {
MagnifyingGlass,
Pause,
Palette,
ClipboardText,
} from "@phosphor-icons/react";

import DOMPurify from "dompurify";
Expand Down Expand Up @@ -377,12 +379,9 @@ export default function ChatMessage(props: ChatMessageProps) {
const preElements = messageRef.current.querySelectorAll("pre > .hljs");
preElements.forEach((preElement) => {
const copyButton = document.createElement("button");
const copyImage = document.createElement("img");
copyImage.src = "/static/copy-button.svg";
copyImage.alt = "Copy";
copyImage.width = 24;
copyImage.height = 24;
copyButton.appendChild(copyImage);
const copyIcon = <ClipboardText size={24} weight="bold" />;
createRoot(copyButton).render(copyIcon);

copyButton.className = `hljs ${styles.codeCopyButton}`;
copyButton.addEventListener("click", () => {
let textContent = preElement.textContent || "";
Expand All @@ -392,7 +391,6 @@ export default function ChatMessage(props: ChatMessageProps) {
textContent = textContent.replace(/^Copy/, "");
textContent = textContent.trim();
navigator.clipboard.writeText(textContent);
copyImage.src = "/static/copy-button-success.svg";
});
preElement.prepend(copyButton);
});
Expand Down
42 changes: 30 additions & 12 deletions src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,12 @@ function FilesMenu(props: FilesMenuProps) {
useEffect(() => {
if (!files) return;

const uniqueFiles = Array.from(new Set(files));

// First, sort lexically
files.sort();
let sortedFiles = files;
uniqueFiles.sort();

let sortedFiles = uniqueFiles;

if (addedFiles) {
sortedFiles = addedFiles.concat(
Expand Down Expand Up @@ -458,12 +461,13 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
);
}

interface ChatSessionActionMenuProps {
export interface ChatSessionActionMenuProps {
conversationId: string;
setTitle: (title: string) => void;
sizing?: "sm" | "md" | "lg";
}

function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
export function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
const [renamedTitle, setRenamedTitle] = useState("");
const [isRenaming, setIsRenaming] = useState(false);
const [isSharing, setIsSharing] = useState(false);
Expand Down Expand Up @@ -596,10 +600,25 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
);
}

function sizeClass() {
switch (props.sizing) {
case "sm":
return "h-4 w-4";
case "md":
return "h-6 w-6";
case "lg":
return "h-8 w-8";
default:
return "h-4 w-4";
}
}

const size = sizeClass();

return (
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
<DropdownMenuTrigger>
<DotsThreeVertical className="h-4 w-4" />
<DotsThreeVertical className={`${size}`} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
Expand All @@ -608,7 +627,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
variant={"ghost"}
onClick={() => setIsRenaming(true)}
>
<Pencil className="mr-2 h-4 w-4" />
<Pencil className={`mr-2 ${size}`} />
Rename
</Button>
</DropdownMenuItem>
Expand All @@ -618,7 +637,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
variant={"ghost"}
onClick={() => setIsSharing(true)}
>
<Share className="mr-2 h-4 w-4" />
<Share className={`mr-2 ${size}`} />
Share
</Button>
</DropdownMenuItem>
Expand All @@ -628,7 +647,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
variant={"ghost"}
onClick={() => setIsDeleting(true)}
>
<Trash className="mr-2 h-4 w-4" />
<Trash className={`mr-2 ${size}`} />
Delete
</Button>
</DropdownMenuItem>
Expand All @@ -640,15 +659,14 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
function ChatSession(props: ChatHistory) {
const [isHovered, setIsHovered] = useState(false);
const [title, setTitle] = useState(props.slug || "New Conversation 🌱");
var currConversationId = parseInt(
new URLSearchParams(window.location.search).get("conversationId") || "-1",
);
var currConversationId =
new URLSearchParams(window.location.search).get("conversationId") || "-1";
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
key={props.conversation_id}
className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === parseInt(props.conversation_id) && currConversationId != -1 ? "dark:bg-neutral-800 bg-white" : ""}`}
className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""}`}
>
<Link
href={`/chat?conversationId=${props.conversation_id}`}
Expand Down
2 changes: 1 addition & 1 deletion src/interface/web/app/share/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export default function SharedChat() {
const chatAPI = "/api/chat?client=web";
const chatAPIBody = {
q: queryToProcess,
conversation_id: parseInt(conversationId),
conversation_id: conversationId,
stream: true,
...(locationData && {
region: locationData.region,
Expand Down
16 changes: 8 additions & 8 deletions src/khoj/database/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ def make_public_conversation_copy(conversation: Conversation):

@staticmethod
def get_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
) -> Optional[Conversation]:
if conversation_id:
conversation = (
Expand All @@ -689,7 +689,7 @@ def get_conversation_sessions(user: KhojUser, client_application: ClientApplicat

@staticmethod
async def aset_conversation_title(
user: KhojUser, client_application: ClientApplication, conversation_id: int, title: str
user: KhojUser, client_application: ClientApplication, conversation_id: str, title: str
):
conversation = await Conversation.objects.filter(
user=user, client=client_application, id=conversation_id
Expand All @@ -701,7 +701,7 @@ async def aset_conversation_title(
return None

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

@staticmethod
Expand Down Expand Up @@ -730,7 +730,7 @@ def create_conversation_session(

@staticmethod
async def aget_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None, title: str = None
user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None, title: str = None
) -> Optional[Conversation]:
query = Conversation.objects.filter(user=user, client=client_application).prefetch_related("agent")

Expand All @@ -747,7 +747,7 @@ async def aget_conversation_by_user(

@staticmethod
async def adelete_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None
user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
):
if conversation_id:
return await Conversation.objects.filter(user=user, client=client_application, id=conversation_id).adelete()
Expand Down Expand Up @@ -900,7 +900,7 @@ def save_conversation(
user: KhojUser,
conversation_log: dict,
client_application: ClientApplication = None,
conversation_id: int = None,
conversation_id: str = None,
user_message: str = None,
):
slug = user_message.strip()[:200] if user_message else None
Expand Down Expand Up @@ -1042,7 +1042,7 @@ async def aset_user_text_to_image_model(user: KhojUser, text_to_image_model_conf
return new_config

@staticmethod
def add_files_to_filter(user: KhojUser, conversation_id: int, files: List[str]):
def add_files_to_filter(user: KhojUser, conversation_id: str, files: List[str]):
conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
for filename in files:
Expand All @@ -1056,7 +1056,7 @@ def add_files_to_filter(user: KhojUser, conversation_id: int, files: List[str]):
return conversation.file_filters

@staticmethod
def remove_files_from_filter(user: KhojUser, conversation_id: int, files: List[str]):
def remove_files_from_filter(user: KhojUser, conversation_id: str, files: List[str]):
conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
for filename in files:
if filename in conversation.file_filters:
Expand Down
Loading

0 comments on commit 06777e1

Please sign in to comment.