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; 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(