Compare commits

...

6 Commits

Author SHA1 Message Date
SubashMohan
c3e9ac0c4c merge main 2026-01-12 17:55:52 +05:30
SubashMohan
8d3ac7804c refactor: Update StickyHeader component styling 2026-01-12 17:43:42 +05:30
SubashMohan
47abe4d219 refactor: Simplify ChatUI and AppLayout components
- Removed conditional rendering of ChatUI in ChatPage for cleaner code.
- Eliminated SmallScreenSidebarFallback component from AppLayout to streamline layout.
- Enhanced ChatUI component with dynamic class names based on chat session state.
2026-01-12 17:39:00 +05:30
SubashMohan
d72ca3040b feat: Introduce ChatHeader component for chat pages 2026-01-12 17:03:25 +05:30
SubashMohan
36a12ea10b style: Simplify ChatHeader component styling
- Consolidated class names for the ChatHeader component to enhance readability.
- Ensured consistent background styling for mobile and desktop views.
2026-01-12 17:03:25 +05:30
SubashMohan
4777305960 refactor: Update app layouts and introduce ChatHeader component
- Renamed and restructured app layout components for clarity.
- Added ChatHeader component for sticky header functionality in chat pages.
- Updated documentation to reflect new component structure and usage.
- Adjusted layout rendering logic for improved mobile responsiveness.
2026-01-12 17:03:25 +05:30
4 changed files with 484 additions and 469 deletions

View File

@@ -0,0 +1,331 @@
"use client";
import { cn, noProp } from "@/lib/utils";
import Text from "@/refresh-components/texts/Text";
import Button from "@/refresh-components/buttons/Button";
import { useCallback, useMemo, useState, useEffect } from "react";
import ShareChatSessionModal from "@/app/chat/components/modal/ShareChatSessionModal";
import IconButton from "@/refresh-components/buttons/IconButton";
import LineItem from "@/refresh-components/buttons/LineItem";
import { useProjectsContext } from "@/app/chat/projects/ProjectsContext";
import useChatSessions from "@/hooks/useChatSessions";
import { usePopup } from "@/components/admin/connectors/Popup";
import {
handleMoveOperation,
shouldShowMoveModal,
showErrorNotification,
} from "@/sections/sidebar/sidebarUtils";
import { LOCAL_STORAGE_KEYS } from "@/sections/sidebar/constants";
import { deleteChatSession } from "@/app/chat/services/lib";
import { useRouter } from "next/navigation";
import MoveCustomAgentChatModal from "@/components/modals/MoveCustomAgentChatModal";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
import { PopoverMenu } from "@/refresh-components/Popover";
import { PopoverSearchInput } from "@/sections/sidebar/ChatButton";
import SimplePopover from "@/refresh-components/SimplePopover";
import { useAppSidebarContext } from "@/refresh-components/contexts/AppSidebarContext";
import useScreenSize from "@/hooks/useScreenSize";
import {
SvgFolderIn,
SvgMoreHorizontal,
SvgShare,
SvgSidebar,
SvgTrash,
} from "@opal/icons";
import { useSettingsContext } from "@/components/settings/SettingsProvider";
import { StickyHeader } from "@/layouts/app-layouts";
/**
* Chat Header Component
*
* Sticky header for chat pages with share, move, and delete actions.
* Rendered inside ChatUI's scroll container for sticky behavior.
*
* Features:
* - Sticky positioning within scroll container
* - Transparent on large screens, solid on small screens
* - Share chat functionality
* - Move chat to project (with confirmation for custom agents)
* - Delete chat with confirmation
* - Responsive sidebar toggle for small screens
*/
export default function ChatHeader() {
const settings = useSettingsContext();
const { isMobile } = useScreenSize();
const { setFolded } = useAppSidebarContext();
const [showShareModal, setShowShareModal] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [showMoveCustomAgentModal, setShowMoveCustomAgentModal] =
useState(false);
const [pendingMoveProjectId, setPendingMoveProjectId] = useState<
number | null
>(null);
const [showMoveOptions, setShowMoveOptions] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverItems, setPopoverItems] = useState<React.ReactNode[]>([]);
const {
projects,
fetchProjects,
refreshCurrentProjectDetails,
currentProjectId,
} = useProjectsContext();
const { currentChatSession, refreshChatSessions, currentChatSessionId } =
useChatSessions();
const { popup, setPopup } = usePopup();
const router = useRouter();
const customHeaderContent =
settings?.enterpriseSettings?.custom_header_content;
const availableProjects = useMemo(() => {
if (!projects) return [];
return projects.filter((project) => project.id !== currentProjectId);
}, [projects, currentProjectId]);
const filteredProjects = useMemo(() => {
if (!searchTerm) return availableProjects;
const term = searchTerm.toLowerCase();
return availableProjects.filter((project) =>
project.name.toLowerCase().includes(term)
);
}, [availableProjects, searchTerm]);
const resetMoveState = useCallback(() => {
setShowMoveOptions(false);
setSearchTerm("");
setPendingMoveProjectId(null);
setShowMoveCustomAgentModal(false);
}, []);
const performMove = useCallback(
async (targetProjectId: number) => {
if (!currentChatSession) return;
try {
await handleMoveOperation(
{
chatSession: currentChatSession,
targetProjectId,
refreshChatSessions,
refreshCurrentProjectDetails,
fetchProjects,
currentProjectId,
},
setPopup
);
resetMoveState();
setPopoverOpen(false);
} catch (error) {
console.error("Failed to move chat session:", error);
}
},
[
currentChatSession,
refreshChatSessions,
refreshCurrentProjectDetails,
fetchProjects,
currentProjectId,
setPopup,
resetMoveState,
]
);
const handleMoveClick = useCallback(
(projectId: number) => {
if (!currentChatSession) return;
if (shouldShowMoveModal(currentChatSession)) {
setPendingMoveProjectId(projectId);
setShowMoveCustomAgentModal(true);
return;
}
void performMove(projectId);
},
[currentChatSession, performMove]
);
const handleDeleteChat = useCallback(async () => {
if (!currentChatSession) return;
try {
const response = await deleteChatSession(currentChatSession.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."
);
}
}, [
currentChatSession,
refreshChatSessions,
fetchProjects,
router,
setPopup,
]);
const setDeleteConfirmationModalOpen = useCallback((open: boolean) => {
setDeleteModalOpen(open);
if (open) {
setPopoverOpen(false);
}
}, []);
useEffect(() => {
const items = showMoveOptions
? [
<PopoverSearchInput
key="search"
setShowMoveOptions={setShowMoveOptions}
onSearch={setSearchTerm}
/>,
...filteredProjects.map((project) => (
<LineItem
key={project.id}
icon={SvgFolderIn}
onClick={noProp(() => handleMoveClick(project.id))}
>
{project.name}
</LineItem>
)),
]
: [
<LineItem
key="move"
icon={SvgFolderIn}
onClick={noProp(() => setShowMoveOptions(true))}
>
Move to Project
</LineItem>,
<LineItem
key="delete"
icon={SvgTrash}
onClick={noProp(() => setDeleteConfirmationModalOpen(true))}
danger
>
Delete
</LineItem>,
];
setPopoverItems(items);
}, [
showMoveOptions,
filteredProjects,
currentChatSession,
setDeleteConfirmationModalOpen,
handleMoveClick,
]);
return (
<>
{popup}
{showShareModal && currentChatSession && (
<ShareChatSessionModal
chatSession={currentChatSession}
onClose={() => setShowShareModal(false)}
/>
)}
{showMoveCustomAgentModal && (
<MoveCustomAgentChatModal
onCancel={resetMoveState}
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>
)}
{(isMobile || customHeaderContent || currentChatSessionId) && (
<StickyHeader
className={cn(!customHeaderContent && "2xl:bg-transparent")}
>
{/* Left - contains the icon-button to fold the AppSidebar on small screens */}
<div className="flex-1">
<IconButton
icon={SvgSidebar}
onClick={() => setFolded(false)}
className={cn(!isMobile && "invisible")}
internal
/>
</div>
{/* Center - contains the custom-header-content */}
<div className="flex-1 flex flex-col items-center overflow-hidden">
<Text
as="p"
text03
mainUiBody
className="text-center break-words w-full"
>
{customHeaderContent}
</Text>
</div>
{/* Right - contains the share and more-options buttons */}
<div
className={cn(
"flex-1 flex flex-row items-center justify-end px-1",
!currentChatSessionId && "invisible"
)}
>
<Button
leftIcon={SvgShare}
transient={showShareModal}
tertiary
onClick={() => setShowShareModal(true)}
>
Share Chat
</Button>
<SimplePopover
trigger={
<IconButton
icon={SvgMoreHorizontal}
className="ml-2"
transient={popoverOpen}
tertiary
/>
}
onOpenChange={(state) => {
setPopoverOpen(state);
if (!state) setShowMoveOptions(false);
}}
side="bottom"
align="end"
>
<PopoverMenu>{popoverItems}</PopoverMenu>
</SimplePopover>
</div>
</StickyHeader>
)}
</>
);
}

View File

@@ -640,20 +640,18 @@ export default function ChatPage({ firstMessage }: ChatPageProps) {
)}
{/* ChatUI */}
{!!currentChatSessionId && (
<ChatUI
ref={chatUiRef}
liveAssistant={liveAssistant}
llmManager={llmManager}
deepResearchEnabled={deepResearchEnabled}
currentMessageFiles={currentMessageFiles}
setPresentingDocument={setPresentingDocument}
onSubmit={onSubmit}
onMessageSelection={onMessageSelection}
stopGenerating={stopGenerating}
handleResubmitLastMessage={handleResubmitLastMessage}
/>
)}
<ChatUI
ref={chatUiRef}
liveAssistant={liveAssistant}
llmManager={llmManager}
deepResearchEnabled={deepResearchEnabled}
currentMessageFiles={currentMessageFiles}
setPresentingDocument={setPresentingDocument}
onSubmit={onSubmit}
onMessageSelection={onMessageSelection}
stopGenerating={stopGenerating}
handleResubmitLastMessage={handleResubmitLastMessage}
/>
{!currentChatSessionId && !currentProjectId && (
<div className="w-full flex-1 flex flex-col items-center justify-end">

View File

@@ -1,20 +1,13 @@
/**
* App Page Layout Component
* App Page Layout Components
*
* Primary layout component for chat/application pages. Handles white-labeling,
* chat session actions (share, move, delete), and responsive header/footer rendering.
*
* Features:
* - Custom header/footer content from enterprise settings
* - Share chat functionality
* - Move chat to project (with confirmation for custom agents)
* - Delete chat with confirmation
* - Mobile-responsive sidebar toggle
* - Conditional rendering based on chat state
* Layout components for chat/application pages including:
* - AppRoot: Main layout wrapper with custom footer
* - StickyHeader: Reusable sticky header wrapper
*
* @example
* ```tsx
* import AppLayouts from "@/layouts/app-layouts";
* import AppLayouts, { StickyHeader } from "@/layouts/app-layouts";
*
* export default function ChatPage() {
* return (
@@ -28,39 +21,9 @@
"use client";
import { cn, ensureHrefProtocol, noProp } from "@/lib/utils";
import { cn, ensureHrefProtocol } from "@/lib/utils";
import type { Components } from "react-markdown";
import Text from "@/refresh-components/texts/Text";
import Button from "@/refresh-components/buttons/Button";
import { useCallback, useMemo, useState, useEffect } from "react";
import ShareChatSessionModal from "@/app/chat/components/modal/ShareChatSessionModal";
import IconButton from "@/refresh-components/buttons/IconButton";
import LineItem from "@/refresh-components/buttons/LineItem";
import { useProjectsContext } from "@/app/chat/projects/ProjectsContext";
import useChatSessions from "@/hooks/useChatSessions";
import { usePopup } from "@/components/admin/connectors/Popup";
import {
handleMoveOperation,
shouldShowMoveModal,
showErrorNotification,
} from "@/sections/sidebar/sidebarUtils";
import { LOCAL_STORAGE_KEYS } from "@/sections/sidebar/constants";
import { deleteChatSession } from "@/app/chat/services/lib";
import { useRouter } from "next/navigation";
import MoveCustomAgentChatModal from "@/components/modals/MoveCustomAgentChatModal";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
import { PopoverMenu } from "@/refresh-components/Popover";
import { PopoverSearchInput } from "@/sections/sidebar/ChatButton";
import SimplePopover from "@/refresh-components/SimplePopover";
import { useAppSidebarContext } from "@/refresh-components/contexts/AppSidebarContext";
import useScreenSize from "@/hooks/useScreenSize";
import {
SvgFolderIn,
SvgMoreHorizontal,
SvgShare,
SvgSidebar,
SvgTrash,
} from "@opal/icons";
import MinimalMarkdown from "@/components/chat/MinimalMarkdown";
import { useSettingsContext } from "@/components/settings/SettingsProvider";
@@ -89,282 +52,27 @@ const footerMarkdownComponents = {
},
} satisfies Partial<Components>;
function AppHeader() {
const settings = useSettingsContext();
const { isMobile } = useScreenSize();
const { setFolded } = useAppSidebarContext();
const [showShareModal, setShowShareModal] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [showMoveCustomAgentModal, setShowMoveCustomAgentModal] =
useState(false);
const [pendingMoveProjectId, setPendingMoveProjectId] = useState<
number | null
>(null);
const [showMoveOptions, setShowMoveOptions] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [popoverOpen, setPopoverOpen] = useState(false);
const [popoverItems, setPopoverItems] = useState<React.ReactNode[]>([]);
const {
projects,
fetchProjects,
refreshCurrentProjectDetails,
currentProjectId,
} = useProjectsContext();
const { currentChatSession, refreshChatSessions, currentChatSessionId } =
useChatSessions();
const { popup, setPopup } = usePopup();
const router = useRouter();
const customHeaderContent =
settings?.enterpriseSettings?.custom_header_content;
const availableProjects = useMemo(() => {
if (!projects) return [];
return projects.filter((project) => project.id !== currentProjectId);
}, [projects, currentProjectId]);
const filteredProjects = useMemo(() => {
if (!searchTerm) return availableProjects;
const term = searchTerm.toLowerCase();
return availableProjects.filter((project) =>
project.name.toLowerCase().includes(term)
);
}, [availableProjects, searchTerm]);
const resetMoveState = useCallback(() => {
setShowMoveOptions(false);
setSearchTerm("");
setPendingMoveProjectId(null);
setShowMoveCustomAgentModal(false);
}, []);
const performMove = useCallback(
async (targetProjectId: number) => {
if (!currentChatSession) return;
try {
await handleMoveOperation(
{
chatSession: currentChatSession,
targetProjectId,
refreshChatSessions,
refreshCurrentProjectDetails,
fetchProjects,
currentProjectId,
},
setPopup
);
resetMoveState();
setPopoverOpen(false);
} catch (error) {
console.error("Failed to move chat session:", error);
}
},
[
currentChatSession,
refreshChatSessions,
refreshCurrentProjectDetails,
fetchProjects,
currentProjectId,
setPopup,
resetMoveState,
]
);
const handleMoveClick = useCallback(
(projectId: number) => {
if (!currentChatSession) return;
if (shouldShowMoveModal(currentChatSession)) {
setPendingMoveProjectId(projectId);
setShowMoveCustomAgentModal(true);
return;
}
void performMove(projectId);
},
[currentChatSession, performMove]
);
const handleDeleteChat = useCallback(async () => {
if (!currentChatSession) return;
try {
const response = await deleteChatSession(currentChatSession.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."
);
}
}, [
currentChatSession,
refreshChatSessions,
fetchProjects,
router,
setPopup,
]);
const setDeleteConfirmationModalOpen = useCallback((open: boolean) => {
setDeleteModalOpen(open);
if (open) {
setPopoverOpen(false);
}
}, []);
useEffect(() => {
const items = showMoveOptions
? [
<PopoverSearchInput
key="search"
setShowMoveOptions={setShowMoveOptions}
onSearch={setSearchTerm}
/>,
...filteredProjects.map((project) => (
<LineItem
key={project.id}
icon={SvgFolderIn}
onClick={noProp(() => handleMoveClick(project.id))}
>
{project.name}
</LineItem>
)),
]
: [
<LineItem
key="move"
icon={SvgFolderIn}
onClick={noProp(() => setShowMoveOptions(true))}
>
Move to Project
</LineItem>,
<LineItem
key="delete"
icon={SvgTrash}
onClick={noProp(() => setDeleteConfirmationModalOpen(true))}
danger
>
Delete
</LineItem>,
];
setPopoverItems(items);
}, [
showMoveOptions,
filteredProjects,
currentChatSession,
setDeleteConfirmationModalOpen,
handleMoveClick,
]);
interface StickyHeaderProps {
children: React.ReactNode;
className?: string;
}
/**
* Sticky Header Component
*
* Reusable sticky header wrapper for chat pages.
* Provides consistent sticky positioning and base styling.
*/
function StickyHeader({ children, className }: StickyHeaderProps) {
return (
<>
{popup}
{showShareModal && currentChatSession && (
<ShareChatSessionModal
chatSession={currentChatSession}
onClose={() => setShowShareModal(false)}
/>
<header
className={cn(
"sticky top-0 z-sticky w-full flex flex-row justify-center items-center py-3 px-4 h-16 bg-background-tint-01",
className
)}
{showMoveCustomAgentModal && (
<MoveCustomAgentChatModal
onCancel={resetMoveState}
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>
)}
{(isMobile || customHeaderContent || currentChatSessionId) && (
<header className="w-full flex flex-row justify-center items-center py-3 px-4 h-16">
{/* Left - contains the icon-button to fold the AppSidebar on mobile */}
<div className="flex-1">
<IconButton
icon={SvgSidebar}
onClick={() => setFolded(false)}
className={cn(!isMobile && "invisible")}
internal
/>
</div>
{/* Center - contains the custom-header-content */}
<div className="flex-1 flex flex-col items-center overflow-hidden">
<Text
as="p"
text03
mainUiBody
className="text-center break-words w-full"
>
{customHeaderContent}
</Text>
</div>
{/* Right - contains the share and more-options buttons */}
<div
className={cn(
"flex-1 flex flex-row items-center justify-end px-1",
!currentChatSessionId && "invisible"
)}
>
<Button
leftIcon={SvgShare}
transient={showShareModal}
tertiary
onClick={() => setShowShareModal(true)}
>
Share Chat
</Button>
<SimplePopover
trigger={
<IconButton
icon={SvgMoreHorizontal}
className="ml-2"
transient={popoverOpen}
tertiary
/>
}
onOpenChange={(state) => {
setPopoverOpen(state);
if (!state) setShowMoveOptions(false);
}}
side="bottom"
align="end"
>
<PopoverMenu>{popoverItems}</PopoverMenu>
</SimplePopover>
</div>
</header>
)}
</>
>
{children}
</header>
);
}
@@ -391,14 +99,11 @@ function AppFooter() {
/**
* App Root Component
*
* Wraps chat pages with white-labeling chrome (custom header/footer) and
* provides chat session management actions.
* Wraps chat pages with custom footer.
*
* Layout Structure:
* ```
* ┌──────────────────────────────────┐
* │ Header (custom or with actions) │
* ├──────────────────────────────────┤
* │ │
* │ Content Area (children) │
* │ │
@@ -407,36 +112,8 @@ function AppFooter() {
* └──────────────────────────────────┘
* ```
*
* Features:
* - Renders custom header content from enterprise settings
* - Shows sidebar toggle on mobile
* - "Share Chat" button for current chat session
* - Kebab menu with "Move to Project" and "Delete" options
* - Move confirmation modal for custom agent chats
* - Delete confirmation modal
* - Renders custom footer disclaimer from enterprise settings
*
* State Management:
* - Manages multiple modals (share, move, delete)
* - Handles project search/filtering in move modal
* - Integrates with projects context for chat operations
* - Uses settings context for white-labeling
* - Uses chat sessions hook for current session
*
* @example
* ```tsx
* // Basic usage in a chat page
* <AppLayouts.Root>
* <ChatInterface />
* </AppLayouts.Root>
*
* // The header will show:
* // - Mobile: Sidebar toggle button
* // - Desktop: Share button + kebab menu (when chat session exists)
* // - Custom header text (if configured)
*
* // The footer will show custom disclaimer (if configured)
* ```
* Note: ChatHeader is rendered inside ChatUI's scroll container
* for sticky behavior, not in this root component.
*/
export interface AppRootProps {
children?: React.ReactNode;
@@ -448,11 +125,10 @@ function AppRoot({ children }: AppRootProps) {
breakout of their immediate containers using cqw units.
*/
<div className="@container flex flex-col h-full w-full">
<AppHeader />
<div className="flex-1 overflow-auto h-full w-full">{children}</div>
<AppFooter />
</div>
);
}
export { AppRoot as Root };
export { AppRoot as Root, StickyHeader };

View File

@@ -33,6 +33,8 @@ import { useUser } from "@/components/user/UserProvider";
import { HORIZON_DISTANCE_PX } from "@/lib/constants";
import Spacer from "@/refresh-components/Spacer";
import { SvgChevronDown } from "@opal/icons";
import ChatHeader from "@/app/chat/components/ChatHeader";
import { cn } from "@/lib/utils";
export interface ChatUIHandle {
scrollToBottom: () => boolean;
@@ -234,7 +236,12 @@ const ChatUI = React.memo(
if (!liveAssistant) return <div className="flex-1" />;
return (
<div className="flex flex-col flex-1 w-full relative overflow-hidden">
<div
className={cn(
"flex flex-col w-full relative overflow-hidden",
currentChatSessionId && "flex-1"
)}
>
{aboveHorizon && (
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 z-sticky">
<IconButton icon={SvgChevronDown} onClick={scrollToBottom} />
@@ -246,119 +253,122 @@ const ChatUI = React.memo(
<div
key={currentChatSessionId}
ref={scrollContainerRef}
className="flex flex-1 justify-center min-h-0 overflow-y-auto overflow-x-hidden default-scrollbar"
className="flex flex-col flex-1 min-h-0 overflow-y-auto overflow-x-hidden default-scrollbar"
onScroll={handleScroll}
>
<div className="w-[min(50rem,100%)] px-4">
{messages.map((message, i) => {
const messageReactComponentKey = `message-${message.nodeId}`;
const parentMessage = message.parentNodeId
? messageTree?.get(message.parentNodeId)
: null;
<ChatHeader />
{currentChatSessionId && (
<div className="w-[min(50rem,100%)] px-4 mx-auto">
{messages.map((message, i) => {
const messageReactComponentKey = `message-${message.nodeId}`;
const parentMessage = message.parentNodeId
? messageTree?.get(message.parentNodeId)
: null;
if (message.type === "user") {
const nextMessage =
messages.length > i + 1 ? messages[i + 1] : null;
if (message.type === "user") {
const nextMessage =
messages.length > i + 1 ? messages[i + 1] : null;
return (
<div
id={messageReactComponentKey}
key={messageReactComponentKey}
>
<HumanMessage
disableSwitchingForStreaming={
(nextMessage && nextMessage.is_generating) || false
}
stopGenerating={stopGenerating}
content={message.message}
files={message.files}
messageId={message.messageId}
nodeId={message.nodeId}
onEdit={handleEditWithMessageId}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
/>
</div>
);
} else if (message.type === "assistant") {
if ((error || loadError) && i === messages.length - 1) {
return (
<div key={`error-${message.nodeId}`} className="p-4">
<ErrorBanner
resubmit={handleResubmitLastMessage}
error={error || loadError || ""}
errorCode={message.errorCode || undefined}
isRetryable={message.isRetryable ?? true}
details={message.errorDetails || undefined}
stackTrace={message.stackTrace || undefined}
<div
id={messageReactComponentKey}
key={messageReactComponentKey}
>
<HumanMessage
disableSwitchingForStreaming={
(nextMessage && nextMessage.is_generating) || false
}
stopGenerating={stopGenerating}
content={message.message}
files={message.files}
messageId={message.messageId}
nodeId={message.nodeId}
onEdit={handleEditWithMessageId}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
/>
</div>
);
} else if (message.type === "assistant") {
if ((error || loadError) && i === messages.length - 1) {
return (
<div key={`error-${message.nodeId}`} className="p-4">
<ErrorBanner
resubmit={handleResubmitLastMessage}
error={error || loadError || ""}
errorCode={message.errorCode || undefined}
isRetryable={message.isRetryable ?? true}
details={message.errorDetails || undefined}
stackTrace={message.stackTrace || undefined}
/>
</div>
);
}
// NOTE: it's fine to use the previous entry in messageHistory
// since this is a "parsed" version of the message tree
// so the previous message is guaranteed to be the parent of the current message
const previousMessage = i !== 0 ? messages[i - 1] : null;
const chatStateData = {
assistant: liveAssistant,
docs: message.documents ?? emptyDocs,
citations: message.citations,
setPresentingDocument,
overriddenModel: llmManager.currentLlm?.modelName,
researchType: message.researchType,
};
return (
<div
id={`message-${message.nodeId}`}
key={messageReactComponentKey}
>
<AIMessage
rawPackets={message.packets}
chatState={chatStateData}
nodeId={message.nodeId}
messageId={message.messageId}
currentFeedback={message.currentFeedback}
llmManager={llmManager}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
onRegenerate={createRegenerator}
parentMessage={previousMessage}
/>
</div>
);
}
})}
// NOTE: it's fine to use the previous entry in messageHistory
// since this is a "parsed" version of the message tree
// so the previous message is guaranteed to be the parent of the current message
const previousMessage = i !== 0 ? messages[i - 1] : null;
const chatStateData = {
assistant: liveAssistant,
docs: message.documents ?? emptyDocs,
citations: message.citations,
setPresentingDocument,
overriddenModel: llmManager.currentLlm?.modelName,
researchType: message.researchType,
};
return (
<div
id={`message-${message.nodeId}`}
key={messageReactComponentKey}
>
<AIMessage
rawPackets={message.packets}
chatState={chatStateData}
nodeId={message.nodeId}
messageId={message.messageId}
currentFeedback={message.currentFeedback}
llmManager={llmManager}
otherMessagesCanSwitchTo={
parentMessage?.childrenNodeIds ?? emptyChildrenIds
}
onMessageSelection={onMessageSelection}
onRegenerate={createRegenerator}
parentMessage={previousMessage}
/>
</div>
);
}
})}
{(((error !== null || loadError !== null) &&
messages[messages.length - 1]?.type === "user") ||
messages[messages.length - 1]?.type === "error") && (
<div className="p-4">
<ErrorBanner
resubmit={handleResubmitLastMessage}
error={error || loadError || ""}
errorCode={
messages[messages.length - 1]?.errorCode || undefined
}
isRetryable={
messages[messages.length - 1]?.isRetryable ?? true
}
details={
messages[messages.length - 1]?.errorDetails || undefined
}
stackTrace={
messages[messages.length - 1]?.stackTrace || undefined
}
/>
</div>
)}
{(((error !== null || loadError !== null) &&
messages[messages.length - 1]?.type === "user") ||
messages[messages.length - 1]?.type === "error") && (
<div className="p-4">
<ErrorBanner
resubmit={handleResubmitLastMessage}
error={error || loadError || ""}
errorCode={
messages[messages.length - 1]?.errorCode || undefined
}
isRetryable={
messages[messages.length - 1]?.isRetryable ?? true
}
details={
messages[messages.length - 1]?.errorDetails || undefined
}
stackTrace={
messages[messages.length - 1]?.stackTrace || undefined
}
/>
</div>
)}
<div ref={endDivRef} />
</div>
<div ref={endDivRef} />
</div>
)}
</div>
</div>
);