Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cloning of message state #2815

Merged
merged 1 commit into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion web/src/app/chat/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,33 @@ export function ChatPage({
const { user, isAdmin, isLoadingUser } = useUser();

const existingChatIdRaw = searchParams.get("chatId");
const [sendOnLoad, setSendOnLoad] = useState<string | null>(
searchParams.get(SEARCH_PARAM_NAMES.SEND_ON_LOAD)
);

const currentPersonaId = searchParams.get(SEARCH_PARAM_NAMES.PERSONA_ID);
const modelVersionFromSearchParams = searchParams.get(
SEARCH_PARAM_NAMES.STRUCTURED_MODEL
);

// Effect to handle sendOnLoad
useEffect(() => {
if (sendOnLoad) {
const newSearchParams = new URLSearchParams(searchParams.toString());
newSearchParams.delete(SEARCH_PARAM_NAMES.SEND_ON_LOAD);

// Update the URL without the send-on-load parameter
router.replace(`?${newSearchParams.toString()}`, { scroll: false });

// Update our local state to reflect the change
setSendOnLoad(null);

// If there's a message, submit it
if (message) {
onSubmit({ messageOverride: message });
}
}
}, [sendOnLoad, searchParams, router]);

const existingChatSessionId = existingChatIdRaw ? existingChatIdRaw : null;

Expand Down Expand Up @@ -196,7 +222,7 @@ export function ChatPage({
};

const llmOverrideManager = useLlmOverride(
user?.preferences.default_model ?? null,
modelVersionFromSearchParams || (user?.preferences.default_model ?? null),
selectedChatSession,
defaultTemperature
);
Expand Down Expand Up @@ -1853,6 +1879,9 @@ export function ChatPage({

{sharedChatSession && (
<ShareChatSessionModal
assistantId={liveAssistant?.id}
message={message}
modelOverride={llmOverrideManager.llmOverride}
chatSessionId={sharedChatSession.id}
existingSharedStatus={sharedChatSession.shared_status}
onClose={() => setSharedChatSession(null)}
Expand All @@ -1867,6 +1896,9 @@ export function ChatPage({
)}
{sharingModalVisible && chatSessionIdRef.current !== null && (
<ShareChatSessionModal
message={message}
assistantId={liveAssistant?.id}
modelOverride={llmOverrideManager.llmOverride}
chatSessionId={chatSessionIdRef.current}
existingSharedStatus={chatSessionSharedStatus}
onClose={() => setSharingModalVisible(false)}
Expand Down
267 changes: 172 additions & 95 deletions web/src/app/chat/modal/ShareChatSessionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { Spinner } from "@/components/Spinner";
import { ChatSessionSharedStatus } from "../interfaces";
import { FiCopy } from "react-icons/fi";
import { CopyButton } from "@/components/CopyButton";
import { SEARCH_PARAM_NAMES } from "../searchParams";
import { usePopup } from "@/components/admin/connectors/Popup";
import { destructureValue, structureValue } from "@/lib/llm/utils";
import { LlmOverride } from "@/lib/hooks";

function buildShareLink(chatSessionId: string) {
const baseUrl = `${window.location.protocol}//${window.location.host}`;
Expand All @@ -26,6 +30,34 @@ async function generateShareLink(chatSessionId: string) {
return null;
}

async function generateCloneLink(
message?: string,
assistantId?: number,
modelOverride?: LlmOverride
) {
const baseUrl = `${window.location.protocol}//${window.location.host}`;
const model = modelOverride
? structureValue(
modelOverride.name,
modelOverride.provider,
modelOverride.modelName
)
: null;
return `${baseUrl}/chat${
message
? `?${SEARCH_PARAM_NAMES.USER_PROMPT}=${encodeURIComponent(message)}`
: ""
}${
assistantId
? `${message ? "&" : "?"}${SEARCH_PARAM_NAMES.PERSONA_ID}=${assistantId}`
: ""
}${
model
? `${message || assistantId ? "&" : "?"}${SEARCH_PARAM_NAMES.STRUCTURED_MODEL}=${encodeURIComponent(model)}`
: ""
}${message ? `&${SEARCH_PARAM_NAMES.SEND_ON_LOAD}=true` : ""}`;
}

async function deleteShareLink(chatSessionId: string) {
const response = await fetch(`/api/chat/chat-session/${chatSessionId}`, {
method: "PATCH",
Expand All @@ -43,117 +75,162 @@ export function ShareChatSessionModal({
existingSharedStatus,
onShare,
onClose,
message,
assistantId,
modelOverride,
}: {
chatSessionId: string;
existingSharedStatus: ChatSessionSharedStatus;
onShare?: (shared: boolean) => void;
onClose: () => void;
message?: string;
assistantId?: number;
modelOverride?: LlmOverride;
}) {
const [linkGenerating, setLinkGenerating] = useState(false);
const [shareLink, setShareLink] = useState<string>(
existingSharedStatus === ChatSessionSharedStatus.Public
? buildShareLink(chatSessionId)
: ""
);
const { popup, setPopup } = usePopup();

return (
<Modal onOutsideClick={onClose} width="max-w-3xl">
<>
<div className="flex mb-4">
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
Share link to Chat
</h2>
</div>

{linkGenerating && <Spinner />}

<div className="flex mt-2">
{shareLink ? (
<div>
<Text>
This chat session is currently shared. Anyone at your
organization can view the message history using the following
link:
</Text>

<div className="flex mt-2">
<CopyButton content={shareLink} />
<a
href={shareLink}
target="_blank"
className="underline text-link mt-1 ml-1 text-sm my-auto"
rel="noreferrer"
<>
{popup}
<Modal onOutsideClick={onClose} width="max-w-3xl">
<>
<div className="flex mb-4">
<h2 className="text-2xl text-emphasis font-bold flex my-auto">
Share link to Chat
</h2>
</div>

<div className="flex mt-2">
{shareLink ? (
<div>
<Text>
This chat session is currently shared. Anyone at your
organization can view the message history using the following
link:
</Text>

<div className="flex mt-2">
<CopyButton content={shareLink} />
<a
href={shareLink}
target="_blank"
className="underline text-link mt-1 ml-1 text-sm my-auto"
rel="noreferrer"
>
{shareLink}
</a>
</div>

<Divider />

<Text className="mb-4">
Click the button below to make the chat private again.
</Text>

<Button
onClick={async () => {
const success = await deleteShareLink(chatSessionId);
if (success) {
setShareLink("");
onShare && onShare(false);
} else {
alert("Failed to delete share link");
}
}}
size="xs"
color="red"
>
{shareLink}
</a>
Delete Share Link
</Button>
</div>

<Divider />

<Text className="mb-4">
Click the button below to make the chat private again.
</Text>

<Button
onClick={async () => {
setLinkGenerating(true);

const success = await deleteShareLink(chatSessionId);
if (success) {
setShareLink("");
onShare && onShare(false);
) : (
<div>
<Callout title="Warning" color="yellow" className="mb-4">
Ensure that all content in the chat is safe to share with the
whole organization. The content of the retrieved documents
will not be visible, but the names of cited documents as well
as the AI and human messages will be visible.
</Callout>
<div className="flex w-full justify-between">
<Button
icon={FiCopy}
onClick={async () => {
// NOTE: for "insecure" non-https setup, the `navigator.clipboard.writeText` may fail
// as the browser may not allow the clipboard to be accessed.
try {
const shareLink =
await generateShareLink(chatSessionId);
if (!shareLink) {
alert("Failed to generate share link");
} else {
setShareLink(shareLink);
onShare && onShare(true);
navigator.clipboard.writeText(shareLink);
}
} catch (e) {
console.error(e);
}
}}
size="xs"
color="green"
>
Generate and Copy Share Link
</Button>
</div>
</div>
)}
</div>

<Divider className="my-4" />
<div className="mb-4">
<Callout title="Clone Chat" color="blue">
Generate a link to clone this chat session with the current query.
This allows others to start a new chat with the same initial
message and settings.
</Callout>
</div>
<div className="flex w-full justify-between">
<Button
icon={FiCopy}
onClick={async () => {
// NOTE: for "insecure" non-https setup, the `navigator.clipboard.writeText` may fail
// as the browser may not allow the clipboard to be accessed.
try {
const cloneLink = await generateCloneLink(
message,
assistantId,
modelOverride
);
if (!cloneLink) {
setPopup({
message: "Failed to generate clone link",
type: "error",
});
} else {
alert("Failed to delete share link");
navigator.clipboard.writeText(cloneLink);
setPopup({
message: "Link copied to clipboard!",
type: "success",
});
}

setLinkGenerating(false);
}}
size="xs"
color="red"
>
Delete Share Link
</Button>
</div>
) : (
<div>
<Callout title="Warning" color="yellow" className="mb-4">
Ensure that all content in the chat is safe to share with the
whole organization. The content of the retrieved documents will
not be visible, but the names of cited documents as well as the
AI and human messages will be visible.
</Callout>

<Button
icon={FiCopy}
onClick={async () => {
setLinkGenerating(true);

// NOTE: for "inescure" non-https setup, the `navigator.clipboard.writeText` may fail
// as the browser may not allow the clipboard to be accessed.
try {
const shareLink = await generateShareLink(chatSessionId);
if (!shareLink) {
alert("Failed to generate share link");
} else {
setShareLink(shareLink);
onShare && onShare(true);
navigator.clipboard.writeText(shareLink);
}
} catch (e) {
console.error(e);
}

setLinkGenerating(false);
}}
size="xs"
color="green"
>
Generate and Copy Share Link
</Button>
</div>
)}
</div>
</>
</Modal>
} catch (e) {
console.error(e);
alert("Failed to generate or copy link.");
}
}}
size="xs"
color="blue"
>
Generate and Copy Clone Link
</Button>
</div>
</>
</Modal>
</>
);
}
2 changes: 2 additions & 0 deletions web/src/app/chat/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ export const SEARCH_PARAM_NAMES = {
TEMPERATURE: "temperature",
MODEL_VERSION: "model-version",
SYSTEM_PROMPT: "system-prompt",
STRUCTURED_MODEL: "structured-model",
// user message
USER_PROMPT: "user-prompt",
SUBMIT_ON_LOAD: "submit-on-load",
// chat title
TITLE: "title",
// for seeding chats
SEEDED: "seeded",
SEND_ON_LOAD: "send-on-load",
};

export function shouldSubmitOnLoad(searchParams: ReadonlyURLSearchParams) {
Expand Down
Loading