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
This commit is contained in:
Debanjum
2025-03-24 17:38:37 +05:30
parent d9c758bcd2
commit 9dfa7757c5
4 changed files with 74 additions and 25 deletions

View File

@@ -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>
);

View File

@@ -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) {

View File

@@ -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>

View File

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