From d9c758bcd2c9631aa918f38e4efa3ee6c53fd420 Mon Sep 17 00:00:00 2001 From: Debanjum Date: Mon, 24 Mar 2025 17:37:08 +0530 Subject: [PATCH 1/2] Create API endpoint to unshare a public conversation Pass isOwner field from the get shared conversation API endpoint if the currently authenticated user created the requested public conversation --- src/khoj/database/adapters/__init__.py | 4 ++++ src/khoj/routers/api_chat.py | 31 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 8fe5787f..572e1f83 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -908,6 +908,10 @@ class PublicConversationAdapters: # Public conversations are viewable by anyone, but not editable. return f"/share/chat/{public_conversation.slug}/" + @staticmethod + def delete_public_conversation_by_slug(user: KhojUser, slug: str): + return PublicConversation.objects.filter(source_owner=user, slug=slug).first().delete() + class ConversationAdapters: @staticmethod diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index acdf48b5..e1961b96 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -11,7 +11,7 @@ from urllib.parse import unquote from asgiref.sync import sync_to_async from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import Response, StreamingResponse +from fastapi.responses import RedirectResponse, Response, StreamingResponse from starlette.authentication import has_required_scope, requires from khoj.app.settings import ALLOWED_HOSTS @@ -255,6 +255,7 @@ def chat_history( "conversation_id": conversation.id, "slug": conversation.title if conversation.title else conversation.slug, "agent": agent_metadata, + "is_owner": conversation.user == user, } ) @@ -332,6 +333,7 @@ def get_shared_chat( "conversation_id": conversation.id, "slug": scrubbed_title, "agent": agent_metadata, + "is_owner": conversation.source_owner == user, } ) @@ -449,6 +451,33 @@ def duplicate_chat_history_public_conversation( ) +@api_chat.delete("/share") +@requires(["authenticated"]) +def delete_public_conversation( + request: Request, + common: CommonQueryParams, + public_conversation_slug: str, +): + user = request.user.object + + # Delete Public Conversation + PublicConversationAdapters.delete_public_conversation_by_slug(user=user, slug=public_conversation_slug) + + update_telemetry_state( + request=request, + telemetry_type="api", + api="delete_chat_share", + **common.__dict__, + ) + + # Redirect to the main chat page + redirect_uri = str(request.app.url_path_for("chat_page")) + return RedirectResponse( + url=redirect_uri, + status_code=301, + ) + + @api_chat.get("/sessions") @requires(["authenticated"]) def chat_sessions( From 9dfa7757c5e405b8d04dbaf8af5c3368fc67914a Mon Sep 17 00:00:00 2001 From: Debanjum Date: Mon, 24 Mar 2025 17:38:37 +0530 Subject: [PATCH 2/2] Unshare public conversations from the title pane on web app Only show the unshare button on public conversations created by the currently logged in user. Otherwise hide the button Set conversation.isOwner = true only if currently logged in user shared the current conversation. This isOwner information is passed by the get shared conversation API endpoint --- .../components/chatHistory/chatHistory.tsx | 39 ++++++++-------- .../components/chatMessage/chatMessage.tsx | 1 + src/interface/web/app/share/chat/page.tsx | 44 ++++++++++++++++++- .../web/app/share/chat/sharedChat.module.css | 15 +++++-- 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 67af2ddb..c1cd923e 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -42,6 +42,7 @@ interface ChatHistoryProps { setAgent: (agent: AgentData) => void; customClassName?: string; setIsChatSideBarOpen?: (isOpen: boolean) => void; + setIsOwner?: (isOwner: boolean) => void; } interface TrainOfThoughtComponentProps { @@ -60,13 +61,13 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { open: { height: "auto", opacity: 1, - transition: { duration: 0.3, ease: "easeOut" } + transition: { duration: 0.3, ease: "easeOut" }, }, closed: { height: 0, opacity: 0, - transition: { duration: 0.3, ease: "easeIn" } - } + transition: { duration: 0.3, ease: "easeIn" }, + }, }; useEffect(() => { @@ -103,17 +104,14 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) { ))} {!collapsed && ( - + {props.trainOfThought.map((train, index) => ( ))} @@ -174,7 +172,6 @@ export default function ChatHistory(props: ChatHistoryProps) { latestUserMessageRef.current?.scrollIntoView({ behavior: "auto", block: "start" }); }); } - }, [data, currentPage]); useEffect(() => { @@ -247,6 +244,7 @@ export default function ChatHistory(props: ChatHistoryProps) { .then((response) => response.json()) .then((chatData: ChatResponse) => { props.setTitle(chatData.response.slug); + props.setIsOwner && props.setIsOwner(chatData?.response?.is_owner); if ( chatData && chatData.response && @@ -274,6 +272,7 @@ export default function ChatHistory(props: ChatHistoryProps) { agent: chatData.response.agent, conversation_id: chatData.response.conversation_id, slug: chatData.response.slug, + is_owner: chatData.response.is_owner, }; props.setAgent(chatData.response.agent); setData(chatMetadata); @@ -312,7 +311,7 @@ export default function ChatHistory(props: ChatHistoryProps) { function constructAgentName() { if (!data || !data.agent || !data.agent?.name) return `Agent`; - if (data.agent.is_hidden) return 'Khoj'; + if (data.agent.is_hidden) return "Khoj"; return data.agent?.name; } @@ -359,8 +358,8 @@ export default function ChatHistory(props: ChatHistoryProps) { md:h-[calc(100svh-theme(spacing.44))] lg:h-[calc(100svh-theme(spacing.72))] `} - ref={scrollAreaRef}> - + ref={scrollAreaRef} + >
@@ -389,12 +388,12 @@ export default function ChatHistory(props: ChatHistoryProps) { index === data.chat.length - 2 ? latestUserMessageRef : // attach ref to the newest fetched message to handle scroll on fetch - // note: stabilize index selection against last page having less messages than fetchMessageCount - index === + // note: stabilize index selection against last page having less messages than fetchMessageCount + index === data.chat.length - - (currentPage - 1) * fetchMessageCount - ? latestFetchedMessageRef - : null + (currentPage - 1) * fetchMessageCount + ? latestFetchedMessageRef + : null } isMobileWidth={isMobileWidth} chatMessage={chatMessage} @@ -472,7 +471,7 @@ export default function ChatHistory(props: ChatHistoryProps) { onDeleteMessage={handleDeleteMessage} customClassName="fullHistory" borderLeftColor={`${data?.agent?.color}-500`} - isLastMessage={index === (props.incomingMessages!.length - 1)} + isLastMessage={index === props.incomingMessages!.length - 1} /> ); diff --git a/src/interface/web/app/components/chatMessage/chatMessage.tsx b/src/interface/web/app/components/chatMessage/chatMessage.tsx index 8c76c551..5d87f7ea 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.tsx +++ b/src/interface/web/app/components/chatMessage/chatMessage.tsx @@ -196,6 +196,7 @@ export interface ChatHistoryData { agent: AgentData; conversation_id: string; slug: string; + is_owner: boolean; } function sendFeedback(uquery: string, kquery: string, sentiment: string) { diff --git a/src/interface/web/app/share/chat/page.tsx b/src/interface/web/app/share/chat/page.tsx index b9b51966..5cb3cee0 100644 --- a/src/interface/web/app/share/chat/page.tsx +++ b/src/interface/web/app/share/chat/page.tsx @@ -22,6 +22,8 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s import { AppSidebar } from "@/app/components/appSidebar/appSidebar"; import { Separator } from "@/components/ui/separator"; import { KhojLogoType } from "@/app/components/logo/khojLogo"; +import { Button } from "@/components/ui/button"; +import { Trash } from "@phosphor-icons/react"; interface ChatBodyDataProps { chatOptionsData: ChatOptions | null; @@ -34,6 +36,37 @@ interface ChatBodyDataProps { conversationId?: string; setQueryToProcess: (query: string) => void; setImages: (images: string[]) => void; + setIsOwner: (isOwner: boolean) => void; +} + +function UnshareButton({ slug, className }: { slug: string; className?: string }) { + const handleUnshare = async () => { + try { + const response = await fetch(`/api/chat/share?public_conversation_slug=${slug}`, { + method: "DELETE", + }); + + if (response.redirected) { + window.location.reload(); + } else { + console.error("Failed to unshare conversation"); + } + } catch (error) { + console.error("Error unsharing conversation:", error); + } + }; + + return ( +
+ +
+ ); } function ChatBodyData(props: ChatBodyDataProps) { @@ -87,6 +120,7 @@ function ChatBodyData(props: ChatBodyDataProps) { conversationId={props.conversationId || ""} setAgent={setAgentMetadata} setTitle={props.setTitle} + setIsOwner={props.setIsOwner} pendingMessage={processingMessage ? message : ""} incomingMessages={props.streamedMessages} customClassName={chatHistoryCustomClassName} @@ -105,7 +139,7 @@ function ChatBodyData(props: ChatBodyDataProps) { agentColor={agentMetadata?.color} isMobileWidth={props.isMobileWidth} setUploadedFiles={props.setUploadedFiles} - setTriggeredAbort={() => { }} + setTriggeredAbort={() => {}} ref={chatInputRef} />
@@ -124,6 +158,7 @@ export default function SharedChat() { const [uploadedFiles, setUploadedFiles] = useState(null); const [paramSlug, setParamSlug] = useState(undefined); const [images, setImages] = useState([]); + const [isOwner, setIsOwner] = useState(false); const { data: authenticatedData, @@ -215,6 +250,12 @@ export default function SharedChat() { > {title} + {isOwner && authenticatedData && ( + + )} ) )} @@ -237,6 +278,7 @@ export default function SharedChat() { setUploadedFiles={setUploadedFiles} isMobileWidth={isMobileWidth} setImages={setImages} + setIsOwner={setIsOwner} />
diff --git a/src/interface/web/app/share/chat/sharedChat.module.css b/src/interface/web/app/share/chat/sharedChat.module.css index d147502b..359277a5 100644 --- a/src/interface/web/app/share/chat/sharedChat.module.css +++ b/src/interface/web/app/share/chat/sharedChat.module.css @@ -93,10 +93,8 @@ div.agentIndicator { padding: 10px; } -@media (max-width: 768px) { - div.chatBody { - grid-template-columns: 0fr 1fr; - } +div.chatTitleWrapper { + grid-template-columns: 1fr auto; } @media screen and (max-width: 768px) { @@ -108,6 +106,15 @@ div.agentIndicator { width: 100%; } + div.chatBody { + grid-template-columns: 0fr 1fr; + } + + div.chatBox { + padding: 0; + height: 100%; + } + div.chatLayout { gap: 0; grid-template-columns: 1fr;