mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 13:25:11 +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;
|
setAgent: (agent: AgentData) => void;
|
||||||
customClassName?: string;
|
customClassName?: string;
|
||||||
setIsChatSideBarOpen?: (isOpen: boolean) => void;
|
setIsChatSideBarOpen?: (isOpen: boolean) => void;
|
||||||
|
setIsOwner?: (isOwner: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrainOfThoughtComponentProps {
|
interface TrainOfThoughtComponentProps {
|
||||||
@@ -60,13 +61,13 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
|
|||||||
open: {
|
open: {
|
||||||
height: "auto",
|
height: "auto",
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: { duration: 0.3, ease: "easeOut" }
|
transition: { duration: 0.3, ease: "easeOut" },
|
||||||
},
|
},
|
||||||
closed: {
|
closed: {
|
||||||
height: 0,
|
height: 0,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: { duration: 0.3, ease: "easeIn" }
|
transition: { duration: 0.3, ease: "easeIn" },
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -103,17 +104,14 @@ function TrainOfThoughtComponent(props: TrainOfThoughtComponentProps) {
|
|||||||
))}
|
))}
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<motion.div
|
<motion.div initial="closed" animate="open" exit="closed" variants={variants}>
|
||||||
initial="closed"
|
|
||||||
animate="open"
|
|
||||||
exit="closed"
|
|
||||||
variants={variants}
|
|
||||||
>
|
|
||||||
{props.trainOfThought.map((train, index) => (
|
{props.trainOfThought.map((train, index) => (
|
||||||
<TrainOfThought
|
<TrainOfThought
|
||||||
key={`train-${index}`}
|
key={`train-${index}`}
|
||||||
message={train}
|
message={train}
|
||||||
primary={index === lastIndex && props.lastMessage && !props.completed}
|
primary={
|
||||||
|
index === lastIndex && props.lastMessage && !props.completed
|
||||||
|
}
|
||||||
agentColor={props.agentColor}
|
agentColor={props.agentColor}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -174,7 +172,6 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
latestUserMessageRef.current?.scrollIntoView({ behavior: "auto", block: "start" });
|
latestUserMessageRef.current?.scrollIntoView({ behavior: "auto", block: "start" });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [data, currentPage]);
|
}, [data, currentPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -247,6 +244,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((chatData: ChatResponse) => {
|
.then((chatData: ChatResponse) => {
|
||||||
props.setTitle(chatData.response.slug);
|
props.setTitle(chatData.response.slug);
|
||||||
|
props.setIsOwner && props.setIsOwner(chatData?.response?.is_owner);
|
||||||
if (
|
if (
|
||||||
chatData &&
|
chatData &&
|
||||||
chatData.response &&
|
chatData.response &&
|
||||||
@@ -274,6 +272,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
agent: chatData.response.agent,
|
agent: chatData.response.agent,
|
||||||
conversation_id: chatData.response.conversation_id,
|
conversation_id: chatData.response.conversation_id,
|
||||||
slug: chatData.response.slug,
|
slug: chatData.response.slug,
|
||||||
|
is_owner: chatData.response.is_owner,
|
||||||
};
|
};
|
||||||
props.setAgent(chatData.response.agent);
|
props.setAgent(chatData.response.agent);
|
||||||
setData(chatMetadata);
|
setData(chatMetadata);
|
||||||
@@ -312,7 +311,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
|
|
||||||
function constructAgentName() {
|
function constructAgentName() {
|
||||||
if (!data || !data.agent || !data.agent?.name) return `Agent`;
|
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;
|
return data.agent?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +358,8 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
md:h-[calc(100svh-theme(spacing.44))]
|
md:h-[calc(100svh-theme(spacing.44))]
|
||||||
lg:h-[calc(100svh-theme(spacing.72))]
|
lg:h-[calc(100svh-theme(spacing.72))]
|
||||||
`}
|
`}
|
||||||
ref={scrollAreaRef}>
|
ref={scrollAreaRef}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className={`${styles.chatHistory} ${props.customClassName}`}>
|
<div className={`${styles.chatHistory} ${props.customClassName}`}>
|
||||||
<div ref={sentinelRef} style={{ height: "1px" }}>
|
<div ref={sentinelRef} style={{ height: "1px" }}>
|
||||||
@@ -389,12 +388,12 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
index === data.chat.length - 2
|
index === data.chat.length - 2
|
||||||
? latestUserMessageRef
|
? latestUserMessageRef
|
||||||
: // attach ref to the newest fetched message to handle scroll on fetch
|
: // attach ref to the newest fetched message to handle scroll on fetch
|
||||||
// note: stabilize index selection against last page having less messages than fetchMessageCount
|
// note: stabilize index selection against last page having less messages than fetchMessageCount
|
||||||
index ===
|
index ===
|
||||||
data.chat.length -
|
data.chat.length -
|
||||||
(currentPage - 1) * fetchMessageCount
|
(currentPage - 1) * fetchMessageCount
|
||||||
? latestFetchedMessageRef
|
? latestFetchedMessageRef
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
isMobileWidth={isMobileWidth}
|
isMobileWidth={isMobileWidth}
|
||||||
chatMessage={chatMessage}
|
chatMessage={chatMessage}
|
||||||
@@ -472,7 +471,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||||||
onDeleteMessage={handleDeleteMessage}
|
onDeleteMessage={handleDeleteMessage}
|
||||||
customClassName="fullHistory"
|
customClassName="fullHistory"
|
||||||
borderLeftColor={`${data?.agent?.color}-500`}
|
borderLeftColor={`${data?.agent?.color}-500`}
|
||||||
isLastMessage={index === (props.incomingMessages!.length - 1)}
|
isLastMessage={index === props.incomingMessages!.length - 1}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export interface ChatHistoryData {
|
|||||||
agent: AgentData;
|
agent: AgentData;
|
||||||
conversation_id: string;
|
conversation_id: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
is_owner: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendFeedback(uquery: string, kquery: string, sentiment: string) {
|
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 { AppSidebar } from "@/app/components/appSidebar/appSidebar";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { KhojLogoType } from "@/app/components/logo/khojLogo";
|
import { KhojLogoType } from "@/app/components/logo/khojLogo";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Trash } from "@phosphor-icons/react";
|
||||||
|
|
||||||
interface ChatBodyDataProps {
|
interface ChatBodyDataProps {
|
||||||
chatOptionsData: ChatOptions | null;
|
chatOptionsData: ChatOptions | null;
|
||||||
@@ -34,6 +36,37 @@ interface ChatBodyDataProps {
|
|||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
setQueryToProcess: (query: string) => void;
|
setQueryToProcess: (query: string) => void;
|
||||||
setImages: (images: 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) {
|
function ChatBodyData(props: ChatBodyDataProps) {
|
||||||
@@ -87,6 +120,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
conversationId={props.conversationId || ""}
|
conversationId={props.conversationId || ""}
|
||||||
setAgent={setAgentMetadata}
|
setAgent={setAgentMetadata}
|
||||||
setTitle={props.setTitle}
|
setTitle={props.setTitle}
|
||||||
|
setIsOwner={props.setIsOwner}
|
||||||
pendingMessage={processingMessage ? message : ""}
|
pendingMessage={processingMessage ? message : ""}
|
||||||
incomingMessages={props.streamedMessages}
|
incomingMessages={props.streamedMessages}
|
||||||
customClassName={chatHistoryCustomClassName}
|
customClassName={chatHistoryCustomClassName}
|
||||||
@@ -105,7 +139,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||||||
agentColor={agentMetadata?.color}
|
agentColor={agentMetadata?.color}
|
||||||
isMobileWidth={props.isMobileWidth}
|
isMobileWidth={props.isMobileWidth}
|
||||||
setUploadedFiles={props.setUploadedFiles}
|
setUploadedFiles={props.setUploadedFiles}
|
||||||
setTriggeredAbort={() => { }}
|
setTriggeredAbort={() => {}}
|
||||||
ref={chatInputRef}
|
ref={chatInputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,6 +158,7 @@ export default function SharedChat() {
|
|||||||
const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | null>(null);
|
const [uploadedFiles, setUploadedFiles] = useState<AttachedFileText[] | null>(null);
|
||||||
const [paramSlug, setParamSlug] = useState<string | undefined>(undefined);
|
const [paramSlug, setParamSlug] = useState<string | undefined>(undefined);
|
||||||
const [images, setImages] = useState<string[]>([]);
|
const [images, setImages] = useState<string[]>([]);
|
||||||
|
const [isOwner, setIsOwner] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: authenticatedData,
|
data: authenticatedData,
|
||||||
@@ -215,6 +250,12 @@ export default function SharedChat() {
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
|
{isOwner && authenticatedData && (
|
||||||
|
<UnshareButton
|
||||||
|
slug={paramSlug}
|
||||||
|
className={"h-4 w-4 mt-1"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -237,6 +278,7 @@ export default function SharedChat() {
|
|||||||
setUploadedFiles={setUploadedFiles}
|
setUploadedFiles={setUploadedFiles}
|
||||||
isMobileWidth={isMobileWidth}
|
isMobileWidth={isMobileWidth}
|
||||||
setImages={setImages}
|
setImages={setImages}
|
||||||
|
setIsOwner={setIsOwner}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,10 +93,8 @@ div.agentIndicator {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
div.chatTitleWrapper {
|
||||||
div.chatBody {
|
grid-template-columns: 1fr auto;
|
||||||
grid-template-columns: 0fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
@@ -108,6 +106,15 @@ div.agentIndicator {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.chatBody {
|
||||||
|
grid-template-columns: 0fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.chatBox {
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
div.chatLayout {
|
div.chatLayout {
|
||||||
gap: 0;
|
gap: 0;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -908,6 +908,10 @@ class PublicConversationAdapters:
|
|||||||
# Public conversations are viewable by anyone, but not editable.
|
# Public conversations are viewable by anyone, but not editable.
|
||||||
return f"/share/chat/{public_conversation.slug}/"
|
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:
|
class ConversationAdapters:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from urllib.parse import unquote
|
|||||||
|
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
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 starlette.authentication import has_required_scope, requires
|
||||||
|
|
||||||
from khoj.app.settings import ALLOWED_HOSTS
|
from khoj.app.settings import ALLOWED_HOSTS
|
||||||
@@ -255,6 +255,7 @@ def chat_history(
|
|||||||
"conversation_id": conversation.id,
|
"conversation_id": conversation.id,
|
||||||
"slug": conversation.title if conversation.title else conversation.slug,
|
"slug": conversation.title if conversation.title else conversation.slug,
|
||||||
"agent": agent_metadata,
|
"agent": agent_metadata,
|
||||||
|
"is_owner": conversation.user == user,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -332,6 +333,7 @@ def get_shared_chat(
|
|||||||
"conversation_id": conversation.id,
|
"conversation_id": conversation.id,
|
||||||
"slug": scrubbed_title,
|
"slug": scrubbed_title,
|
||||||
"agent": agent_metadata,
|
"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")
|
@api_chat.get("/sessions")
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
def chat_sessions(
|
def chat_sessions(
|
||||||
|
|||||||
Reference in New Issue
Block a user