mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
Enable unsharing of a public conversation (#1135)
This change enables the creator of a shared conversation to stop sharing the conversation publicly. ### Details 1. Create an API endpoint to enable the owner of the shared conversation to unshare it 2. Unshare a public conversations from the title pane of the public conversation on the web app
This commit is contained in:
@@ -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) {
|
||||
))}
|
||||
<AnimatePresence initial={false}>
|
||||
{!collapsed && (
|
||||
<motion.div
|
||||
initial="closed"
|
||||
animate="open"
|
||||
exit="closed"
|
||||
variants={variants}
|
||||
>
|
||||
<motion.div initial="closed" animate="open" exit="closed" variants={variants}>
|
||||
{props.trainOfThought.map((train, index) => (
|
||||
<TrainOfThought
|
||||
key={`train-${index}`}
|
||||
message={train}
|
||||
primary={index === lastIndex && props.lastMessage && !props.completed}
|
||||
primary={
|
||||
index === lastIndex && props.lastMessage && !props.completed
|
||||
}
|
||||
agentColor={props.agentColor}
|
||||
/>
|
||||
))}
|
||||
@@ -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}
|
||||
>
|
||||
<div>
|
||||
<div className={`${styles.chatHistory} ${props.customClassName}`}>
|
||||
<div ref={sentinelRef} style={{ height: "1px" }}>
|
||||
@@ -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}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="p-0 text-sm h-auto text-rose-500 hover:text-rose-600"
|
||||
variant={"ghost"}
|
||||
onClick={handleUnshare}
|
||||
>
|
||||
<Trash className={`${className}`} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
@@ -124,6 +158,7 @@ export default function SharedChat() {
|
||||
const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | null>(null);
|
||||
const [paramSlug, setParamSlug] = useState<string | undefined>(undefined);
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [isOwner, setIsOwner] = useState(false);
|
||||
|
||||
const {
|
||||
data: authenticatedData,
|
||||
@@ -215,6 +250,12 @@ export default function SharedChat() {
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
{isOwner && authenticatedData && (
|
||||
<UnshareButton
|
||||
slug={paramSlug}
|
||||
className={"h-4 w-4 mt-1"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
@@ -237,6 +278,7 @@ export default function SharedChat() {
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
setImages={setImages}
|
||||
setIsOwner={setIsOwner}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user