Compare commits

...

5 Commits

Author SHA1 Message Date
Raunak Bhagat
82f7aa13f0 Update right section 2025-11-18 17:31:01 -08:00
Raunak Bhagat
511e01e4ba Finalize header updates 2025-11-18 17:27:34 -08:00
Raunak Bhagat
c9b3ade60b Update share button style 2025-11-18 15:49:35 -08:00
Raunak Bhagat
a2ce9aa410 Wrap chat/page and shared/page.tsx with ShareChatWrapper 2025-11-18 15:47:23 -08:00
Raunak Bhagat
b3b14b6683 Add a dynamically settable right-header-section 2025-11-18 15:35:10 -08:00
8 changed files with 501 additions and 75 deletions

View File

@@ -162,6 +162,20 @@ export interface BackendChatSession {
packets: Packet[][];
}
export function toChatSession(backend: BackendChatSession): ChatSession {
return {
id: backend.chat_session_id,
name: backend.description,
persona_id: backend.persona_id,
time_created: backend.time_created,
time_updated: backend.time_updated,
shared_status: backend.shared_status,
project_id: null,
current_alternate_model: backend.current_alternate_model ?? "",
current_temperature_override: backend.current_temperature_override,
};
}
export interface BackendMessage {
message_id: number;
message_type: string;

View File

@@ -5,6 +5,7 @@ import { ChatProvider } from "@/refresh-components/contexts/ChatContext";
import { ProjectsProvider } from "./projects/ProjectsContext";
import AppSidebar from "@/sections/sidebar/AppSidebar";
import AppLayout from "@/refresh-components/layouts/AppLayout";
import { HeaderActionsProvider } from "@/refresh-components/contexts/HeaderActionsContext";
export interface LayoutProps {
children: React.ReactNode;
@@ -61,7 +62,9 @@ export default async function Layout({ children }: LayoutProps) {
<ProjectsProvider initialProjects={projects}>
<div className="flex flex-row w-full h-full">
<AppSidebar />
<AppLayout>{children}</AppLayout>
<HeaderActionsProvider>
<AppLayout>{children}</AppLayout>
</HeaderActionsProvider>
</div>
</ProjectsProvider>
</ChatProvider>

View File

@@ -1,10 +1,40 @@
import ChatSessionLayout from "@/refresh-components/layouts/ChatSessionLayout";
import ChatPage from "./components/ChatPage";
import { SEARCH_PARAM_NAMES } from "./services/searchParams";
import { fetchSS } from "@/lib/utilsSS";
import { BackendChatSession, ChatSession, toChatSession } from "./interfaces";
export default async function Page(props: {
export interface PageProps {
searchParams: Promise<{ [key: string]: string }>;
}) {
}
async function fetchChatSession(
chatSessionId: string
): Promise<ChatSession | null> {
try {
const response = await fetchSS(`/chat/get-chat-session/${chatSessionId}`);
if (!response.ok) {
return null;
}
const backendSession: BackendChatSession = await response.json();
return toChatSession(backendSession);
} catch (error) {
console.error("Failed to fetch chat session:", error);
return null;
}
}
export default async function Page(props: PageProps) {
const searchParams = await props.searchParams;
const firstMessage = searchParams.firstMessage;
const chatSessionId = searchParams[SEARCH_PARAM_NAMES.CHAT_ID] ?? null;
const chatSession = chatSessionId
? await fetchChatSession(chatSessionId)
: null;
return <ChatPage firstMessage={firstMessage} />;
return (
<ChatSessionLayout chatSession={chatSession}>
<ChatPage firstMessage={firstMessage} />
</ChatSessionLayout>
);
}

View File

@@ -1,8 +1,7 @@
import { fetchSS } from "@/lib/utilsSS";
import { redirect } from "next/navigation";
import { requireAuth } from "@/lib/auth/requireAuth";
import { BackendChatSession } from "../../interfaces";
import { SharedChatDisplay } from "./SharedChatDisplay";
import { SharedChatDisplay } from "@/app/chat/shared/[chatId]/SharedChatDisplay";
import { Persona } from "@/app/admin/assistants/interfaces";
import { constructMiniFiedPersona } from "@/lib/assistantIconUtils";
@@ -16,9 +15,11 @@ async function getSharedChat(chatId: string) {
return null;
}
export default async function Page(props: {
export interface PageProps {
params: Promise<{ chatId: string }>;
}) {
}
export default async function Page(props: PageProps) {
const params = await props.params;
const authResult = await requireAuth();

View File

@@ -8,12 +8,17 @@ import {
} from "@/components/ui/popover";
export interface SimplePopoverProps
extends React.ComponentPropsWithoutRef<typeof PopoverContent> {
extends Omit<
React.ComponentPropsWithoutRef<typeof PopoverContent>,
"children"
> {
trigger: React.ReactNode | ((open: boolean) => React.ReactNode);
children: React.ReactNode | ((close: () => void) => React.ReactNode);
}
export default function SimplePopover({
trigger,
children,
...rest
}: SimplePopoverProps) {
const [open, setOpen] = useState(false);
@@ -23,7 +28,11 @@ export default function SimplePopover({
<PopoverTrigger asChild>
<div>{typeof trigger === "function" ? trigger(open) : trigger}</div>
</PopoverTrigger>
<PopoverContent align="start" side="top" {...rest} />
<PopoverContent align="start" side="top" {...rest}>
{typeof children === "function"
? children(() => setOpen(false))
: children}
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,104 @@
"use client";
import React, {
ReactNode,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
interface HeaderActionsState {
node: ReactNode | null;
reserveSpace: boolean;
}
interface HeaderActionsContextValue extends HeaderActionsState {
setHeaderActions: (node: ReactNode | null) => void;
reserveHeaderSpace: () => void;
clearHeaderActions: () => void;
}
const HeaderActionsContext = createContext<HeaderActionsContextValue | null>(
null
);
export function HeaderActionsProvider({
children,
}: {
children: React.ReactNode;
}) {
const [state, setState] = useState<HeaderActionsState>({
node: null,
reserveSpace: false,
});
const setHeaderActions = useCallback<
HeaderActionsContextValue["setHeaderActions"]
>((node) => {
setState((prev) => ({
...prev,
node,
}));
}, []);
const reserveHeaderSpace = useCallback(() => {
setState((prev) => ({
...prev,
reserveSpace: true,
}));
}, []);
const clearHeaderActions = useCallback(() => {
setState({
node: null,
reserveSpace: false,
});
}, []);
const value = useMemo<HeaderActionsContextValue>(
() => ({
...state,
setHeaderActions,
reserveHeaderSpace,
clearHeaderActions,
}),
[state, setHeaderActions, reserveHeaderSpace, clearHeaderActions]
);
return (
<HeaderActionsContext.Provider value={value}>
{children}
</HeaderActionsContext.Provider>
);
}
export function useHeaderActions() {
const context = useContext(HeaderActionsContext);
if (!context) {
throw new Error(
"useHeaderActions must be used within a HeaderActionsProvider"
);
}
return {
setHeaderActions: context.setHeaderActions,
reserveHeaderSpace: context.reserveHeaderSpace,
clearHeaderActions: context.clearHeaderActions,
};
}
export function useHeaderActionsValue() {
const context = useContext(HeaderActionsContext);
if (!context) {
throw new Error(
"useHeaderActionsValue must be used within a HeaderActionsProvider"
);
}
return {
headerNode: context.node,
reserveSpace: context.reserveSpace,
};
}

View File

@@ -4,12 +4,7 @@ import { useSettingsContext } from "@/components/settings/SettingsProvider";
import { cn } from "@/lib/utils";
import Text from "@/refresh-components/texts/Text";
import { FOLDED_SIZE } from "@/refresh-components/Logo";
import { useAppFocus } from "@/lib/hooks";
import IconButton from "@/refresh-components/buttons/IconButton";
import SvgShare from "@/icons/share";
import { useChatContext } from "@/refresh-components/contexts/ChatContext";
import { useState } from "react";
import ShareChatSessionModal from "@/app/chat/components/modal/ShareChatSessionModal";
import { useHeaderActionsValue } from "@/refresh-components/contexts/HeaderActionsContext";
export default function AppLayout({
className,
@@ -23,69 +18,50 @@ export default function AppLayout({
settings.enterpriseSettings?.custom_lower_disclaimer_content;
const customLogo = settings.enterpriseSettings?.use_custom_logo;
const appFocus = useAppFocus();
const { chatSessions } = useChatContext();
const [showShareModal, setShowShareModal] = useState(false);
const currentChatSession =
typeof appFocus === "object" && appFocus.type === "chat"
? chatSessions.find((session) => session.id === appFocus.id)
: undefined;
const { headerNode, reserveSpace } = useHeaderActionsValue();
const shouldRenderHeader =
!!customHeaderContent || reserveSpace || !!headerNode;
return (
<>
{showShareModal && currentChatSession && (
<ShareChatSessionModal
chatSession={currentChatSession}
onClose={() => setShowShareModal(false)}
/>
<div className="flex flex-col h-full w-full">
{/* Header */}
{shouldRenderHeader && (
<header className="w-full flex flex-row justify-center items-center py-3 px-4 h-16">
<div className="flex-1" />
<div className="flex-1 flex flex-col items-center justify-center">
{customHeaderContent && <Text text03>{customHeaderContent}</Text>}
</div>
<div className="flex-1 flex flex-row items-center justify-end px-1">
{headerNode}
</div>
</header>
)}
<div className="flex flex-col h-full w-full">
{/* Header */}
{(customHeaderContent || currentChatSession) && (
<header className="w-full flex flex-row justify-center items-center py-3 px-4">
<div className="flex-1" />
<div className="flex-1 flex flex-col items-center">
<Text text03>{customHeaderContent}</Text>
</div>
<div className="flex-1 flex flex-row items-center justify-end px-1">
<IconButton
icon={SvgShare}
transient={showShareModal}
tertiary
onClick={() => setShowShareModal(true)}
/>
</div>
</header>
)}
<div className={cn("flex-1 overflow-auto", className)} {...rest}>
{children}
</div>
{(customLogo || customFooterContent) && (
<footer className="w-full flex flex-row justify-center items-center gap-2 py-3">
{customLogo && (
<img
src="/api/enterprise-settings/logo"
alt="Logo"
style={{
objectFit: "contain",
height: FOLDED_SIZE,
width: FOLDED_SIZE,
}}
className="flex-shrink-0"
/>
)}
{customFooterContent && (
<Text text03 secondaryBody>
{customFooterContent}
</Text>
)}
</footer>
)}
<div className={cn("flex-1 overflow-auto", className)} {...rest}>
{children}
</div>
</>
{(customLogo || customFooterContent) && (
<footer className="w-full flex flex-row justify-center items-center gap-2 py-3">
{customLogo && (
<img
src="/api/enterprise-settings/logo"
alt="Logo"
style={{
objectFit: "contain",
height: FOLDED_SIZE,
width: FOLDED_SIZE,
}}
className="flex-shrink-0"
/>
)}
{customFooterContent && (
<Text text03 secondaryBody>
{customFooterContent}
</Text>
)}
</footer>
)}
</div>
);
}

View File

@@ -0,0 +1,289 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ChatSession } from "@/app/chat/interfaces";
import {
useHeaderActions,
useHeaderActionsValue,
} from "@/refresh-components/contexts/HeaderActionsContext";
import Button from "@/refresh-components/buttons/Button";
import SvgShare from "@/icons/share";
import ShareChatSessionModal from "@/app/chat/components/modal/ShareChatSessionModal";
import SimplePopover from "@/refresh-components/SimplePopover";
import IconButton from "@/refresh-components/buttons/IconButton";
import SvgMoreHorizontal from "@/icons/more-horizontal";
import MenuButton from "@/refresh-components/buttons/MenuButton";
import SvgFolderIn from "@/icons/folder-in";
import SvgTrash from "@/icons/trash";
import { useProjectsContext } from "@/app/chat/projects/ProjectsContext";
import { useChatContext } from "@/refresh-components/contexts/ChatContext";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { deleteChatSession } from "@/app/chat/services/lib";
import {
handleMoveOperation,
shouldShowMoveModal,
showErrorNotification,
} from "@/sections/sidebar/sidebarUtils";
import { LOCAL_STORAGE_KEYS } from "@/sections/sidebar/constants";
import MoveCustomAgentChatModal from "@/components/modals/MoveCustomAgentChatModal";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
import { Modal } from "@/components/Modal";
import Text from "@/refresh-components/texts/Text";
interface ChatSessionLayoutProps {
chatSession: ChatSession | null;
children: React.ReactNode;
reserveHeaderSpace?: boolean;
}
export default function ChatSessionLayout({
chatSession,
children,
reserveHeaderSpace = true,
}: ChatSessionLayoutProps) {
const {
setHeaderActions,
reserveHeaderSpace: reserveSlot,
clearHeaderActions,
} = useHeaderActions();
const { reserveSpace } = useHeaderActionsValue();
const [showShareModal, setShowShareModal] = useState(false);
const [moveModalOpen, setMoveModalOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [pendingMoveProjectId, setPendingMoveProjectId] = useState<
number | null
>(null);
const [showMoveCustomAgentModal, setShowMoveCustomAgentModal] =
useState(false);
const {
projects,
fetchProjects,
refreshCurrentProjectDetails,
currentProjectId,
} = useProjectsContext();
const { refreshChatSessions } = useChatContext();
const { popup, setPopup } = usePopup();
const router = useRouter();
useEffect(() => {
if (!reserveHeaderSpace) {
return;
}
reserveSlot();
return () => {
clearHeaderActions();
};
}, [reserveHeaderSpace, reserveSlot, clearHeaderActions]);
useEffect(() => {
if (!chatSession) {
setHeaderActions(null);
return;
}
const actions = (
<div className="flex flex-row items-center">
<Button
leftIcon={SvgShare}
transient={showShareModal}
tertiary
onClick={() => setShowShareModal(true)}
>
Share Chat
</Button>
<SimplePopover
trigger={(open) => (
<IconButton icon={SvgMoreHorizontal} tertiary transient={open} />
)}
>
{(close) => (
<div className="flex flex-col gap-1 min-w-[12rem]">
<MenuButton
icon={SvgFolderIn}
onClick={() => {
close();
setMoveModalOpen(true);
}}
>
Move to Project
</MenuButton>
<MenuButton
icon={SvgTrash}
danger
onClick={() => {
close();
setDeleteModalOpen(true);
}}
>
Delete
</MenuButton>
</div>
)}
</SimplePopover>
</div>
);
setHeaderActions(actions);
return () => {
setHeaderActions(null);
};
}, [chatSession, showShareModal, setHeaderActions, reserveSpace]);
const performMove = useCallback(
async (targetProjectId: number) => {
if (!chatSession) {
return;
}
try {
await handleMoveOperation(
{
chatSession,
targetProjectId,
refreshChatSessions,
refreshCurrentProjectDetails,
fetchProjects,
currentProjectId,
},
setPopup
);
setMoveModalOpen(false);
} catch (error) {
console.error("Failed to move chat session:", error);
} finally {
setPendingMoveProjectId(null);
setShowMoveCustomAgentModal(false);
}
},
[
chatSession,
refreshChatSessions,
refreshCurrentProjectDetails,
fetchProjects,
currentProjectId,
setPopup,
]
);
const handleMoveSelection = useCallback(
(projectId: number) => {
if (!chatSession) return;
if (shouldShowMoveModal(chatSession)) {
setPendingMoveProjectId(projectId);
setShowMoveCustomAgentModal(true);
return;
}
void performMove(projectId);
},
[chatSession, performMove]
);
const handleDeleteChat = useCallback(async () => {
if (!chatSession) return;
try {
const response = await deleteChatSession(chatSession.id);
if (!response.ok) {
throw new Error("Failed to delete chat session");
}
await Promise.all([refreshChatSessions(), fetchProjects()]);
router.replace("/chat");
setDeleteModalOpen(false);
} catch (error) {
console.error("Failed to delete chat:", error);
showErrorNotification(
setPopup,
"Failed to delete chat. Please try again."
);
}
}, [chatSession, refreshChatSessions, fetchProjects, router, setPopup]);
const handleMoveModalClose = useCallback(() => {
setMoveModalOpen(false);
setPendingMoveProjectId(null);
setShowMoveCustomAgentModal(false);
}, []);
const projectsWithoutCurrent = useMemo(() => {
if (!projects) return [];
return projects.filter((project) => project.id !== currentProjectId);
}, [projects, currentProjectId]);
return (
<>
{popup}
{chatSession && showShareModal && (
<ShareChatSessionModal
chatSession={chatSession}
onClose={() => setShowShareModal(false)}
/>
)}
{moveModalOpen && (
<Modal
title="Move Chat to Project"
onOutsideClick={handleMoveModalClose}
width="max-w-md"
hideDividerForTitle
>
<div className="flex flex-col gap-3">
<Text text03>
Choose a project to move <b>{chatSession?.name || "this chat"}</b>{" "}
into.
</Text>
<div className="flex flex-col gap-1">
{projectsWithoutCurrent.length === 0 && (
<Text text03>No available projects.</Text>
)}
{projectsWithoutCurrent.map((project) => (
<MenuButton
key={project.id}
icon={SvgFolderIn}
onClick={() => handleMoveSelection(project.id)}
>
{project.name}
</MenuButton>
))}
</div>
<div className="flex justify-end">
<Button tertiary onClick={handleMoveModalClose}>
Cancel
</Button>
</div>
</div>
</Modal>
)}
{showMoveCustomAgentModal && (
<MoveCustomAgentChatModal
onCancel={handleMoveModalClose}
onConfirm={async (doNotShowAgain: boolean) => {
if (doNotShowAgain && typeof window !== "undefined") {
window.localStorage.setItem(
LOCAL_STORAGE_KEYS.HIDE_MOVE_CUSTOM_AGENT_MODAL,
"true"
);
}
if (pendingMoveProjectId != null) {
await performMove(pendingMoveProjectId);
}
}}
/>
)}
{deleteModalOpen && (
<ConfirmationModalLayout
title="Delete Chat"
icon={SvgTrash}
onClose={() => setDeleteModalOpen(false)}
submit={
<Button danger onClick={handleDeleteChat}>
Delete
</Button>
}
>
Are you sure you want to delete this chat? This action cannot be
undone.
</ConfirmationModalLayout>
)}
{children}
</>
);
}