Compare commits

...

6 Commits

Author SHA1 Message Date
Raunak Bhagat
9e9c3ec0b9 Remove unused imports 2025-11-18 13:51:10 -08:00
Raunak Bhagat
1457ca2a20 Make share button instantaneous 2025-11-18 13:50:37 -08:00
Raunak Bhagat
edc390edc6 Implement AppPage wrapper for all other pages inside of /chat 2025-11-18 13:34:38 -08:00
Raunak Bhagat
022624cb5a Maintain consistent heights 2025-11-18 13:20:09 -08:00
Raunak Bhagat
f301257130 Make chatSession info and settings info be passed in as server-side data 2025-11-18 13:07:52 -08:00
Raunak Bhagat
9eecc71cda Fix flashing 2025-11-18 11:43:49 -08:00
14 changed files with 215 additions and 147 deletions

View File

@@ -1,5 +1,18 @@
import AgentsPage from "@/refresh-pages/AgentsPage";
import { fetchSettingsSS } from "@/components/settings/lib";
import AppPage from "@/refresh-components/layouts/AppPage";
export default function Page() {
return <AgentsPage />;
export default async function Page() {
const settings = await fetchSettingsSS();
const appPageProps = {
chatSession: null,
settings,
};
return (
<AppPage {...appPageProps}>
<AgentsPage />
</AppPage>
);
}

View File

@@ -17,7 +17,12 @@ import {
} from "react";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
import { useFederatedConnectors, useFilters, useLlmManager } from "@/lib/hooks";
import {
useFederatedConnectors,
useFilters,
useLlmManager,
useShowCenteredInput,
} from "@/lib/hooks";
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
import { FiArrowDown } from "react-icons/fi";
import { OnyxDocument, MinimalOnyxDocument } from "@/lib/search/interfaces";
@@ -52,12 +57,8 @@ import {
} from "@/app/chat/stores/useChatSessionStore";
import {
useCurrentChatState,
useSubmittedMessage,
useLoadingError,
useIsReady,
useIsFetching,
useCurrentMessageTree,
useCurrentMessageHistory,
useHasPerformedInitialScroll,
useDocumentSidebarVisible,
useHasSentLocalUserMessage,
@@ -405,14 +406,10 @@ export default function ChatPage({
// Access chat state directly from the store
const currentChatState = useCurrentChatState();
const chatSessionId = useChatSessionStore((state) => state.currentSessionId);
const submittedMessage = useSubmittedMessage();
const loadingError = useLoadingError();
const uncaughtError = useUncaughtError();
const isReady = useIsReady();
const maxTokens = useMaxTokens();
const isFetchingChatMessages = useIsFetching();
const completeMessageTree = useCurrentMessageTree();
const messageHistory = useCurrentMessageHistory();
const hasPerformedInitialScroll = useHasPerformedInitialScroll();
const currentSessionHasSentLocalUserMessage = useHasSentLocalUserMessage();
const documentSidebarVisible = useDocumentSidebarVisible();
@@ -422,6 +419,8 @@ export default function ChatPage({
const updateCurrentDocumentSidebarVisible = useChatSessionStore(
(state) => state.updateCurrentDocumentSidebarVisible
);
const { messageHistory, loadingError, showCenteredInput } =
useShowCenteredInput();
const clientScrollToBottom = useCallback(
(fast?: boolean) => {
@@ -622,13 +621,6 @@ export default function ChatPage({
setTimeout(() => updateCurrentDocumentSidebarVisible(false), 300);
}, [updateCurrentDocumentSidebarVisible]);
// Determine whether to show the centered input (no messages yet)
const showCenteredInput =
messageHistory.length === 0 &&
!isFetchingChatMessages &&
!loadingError &&
!submittedMessage;
// Only show the centered hero layout when there is NO project selected
// and there are no messages yet. If a project is selected, prefer a top layout.
const showCenteredHero = currentProjectId === null && showCenteredInput;

View File

@@ -1,15 +1,18 @@
"use client";
import { fetchSettingsSS } from "@/components/settings/lib";
import InputPrompts from "@/app/chat/input-prompts/InputPrompts";
import AppPage from "@/refresh-components/layouts/AppPage";
import InputPrompts from "./InputPrompts";
export default async function InputPromptsPage() {
const settings = await fetchSettingsSS();
const appPageProps = {
chatSession: null,
settings,
};
export default function InputPromptsPage() {
return (
<div className="w-full py-16">
<div className="px-32">
<div className="mx-auto container">
<InputPrompts />
</div>
</div>
</div>
<AppPage {...appPageProps} className="w-full px-32 py-16 mx-auto container">
<InputPrompts />
</AppPage>
);
}

View File

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

View File

@@ -4,7 +4,6 @@ import { fetchChatData } from "@/lib/chat/fetchChatData";
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";
export interface LayoutProps {
children: React.ReactNode;
@@ -20,9 +19,7 @@ export default async function Layout({ children }: LayoutProps) {
safeSearchParams as { [key: string]: string }
);
if ("redirect" in data) {
redirect(data.redirect);
}
if ("redirect" in data) redirect(data.redirect);
const {
chatSessions,
@@ -61,7 +58,7 @@ export default async function Layout({ children }: LayoutProps) {
<ProjectsProvider initialProjects={projects}>
<div className="flex flex-row w-full h-full">
<AppSidebar />
<AppLayout>{children}</AppLayout>
{children}
</div>
</ProjectsProvider>
</ChatProvider>

View File

@@ -3,18 +3,25 @@ import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { cookies } from "next/headers";
import NRFPage from "./NRFPage";
import { NRFPreferencesProvider } from "../../../components/context/NRFPreferencesContext";
import { fetchSettingsSS } from "@/components/settings/lib";
import AppPage from "@/refresh-components/layouts/AppPage";
export default async function Page() {
noStore();
const requestCookies = await cookies();
const settings = await fetchSettingsSS();
const appPageProps = {
settings,
chatSession: null,
};
return (
<div className="w-full h-full bg-black">
<AppPage {...appPageProps}>
<InstantSSRAutoRefresh />
<NRFPreferencesProvider>
<NRFPage requestCookies={requestCookies} />
</NRFPreferencesProvider>
</div>
</AppPage>
);
}

View File

@@ -1,10 +1,30 @@
import ChatPage from "./components/ChatPage";
import AppPage from "@/refresh-components/layouts/AppPage";
import ChatPage from "@/app/chat/components/ChatPage";
import { SEARCH_PARAM_NAMES } from "./services/searchParams";
import { fetchSettingsSS } from "@/components/settings/lib";
import { fetchChatSessionSS } from "@/lib/chat/fetchChatSessionSS";
export default async function Page(props: {
export interface PageProps {
searchParams: Promise<{ [key: string]: string }>;
}) {
}
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 settings = await fetchSettingsSS();
const chatSession = chatSessionId
? await fetchChatSessionSS(chatSessionId)
: null;
return <ChatPage firstMessage={firstMessage} />;
const appPageProps = {
chatSession,
settings,
};
return (
<AppPage {...appPageProps}>
<ChatPage firstMessage={firstMessage} />
</AppPage>
);
}

View File

@@ -1,24 +1,18 @@
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 { Persona } from "@/app/admin/assistants/interfaces";
import { constructMiniFiedPersona } from "@/lib/assistantIconUtils";
import AppPage from "@/refresh-components/layouts/AppPage";
import { fetchSettingsSS } from "@/components/settings/lib";
import { fetchBackendChatSessionSS } from "@/lib/chat/fetchChatSessionSS";
import { toChatSession } from "@/app/chat/interfaces";
async function getSharedChat(chatId: string) {
const response = await fetchSS(
`/chat/get-chat-session/${chatId}?is_shared=True`
);
if (response.ok) {
return await response.json();
}
return null;
export interface PageProps {
params: Promise<{ chatId: string }>;
}
export default async function Page(props: {
params: Promise<{ chatId: string }>;
}) {
export default async function Page(props: PageProps) {
const params = await props.params;
const authResult = await requireAuth();
@@ -28,14 +22,29 @@ export default async function Page(props: {
// Catch cases where backend is completely unreachable
// Allows render instead of throwing an exception and crashing
const chatSession = await getSharedChat(params.chatId).catch(() => null);
const backendChatSession = await fetchBackendChatSessionSS(
params.chatId,
true
).catch(() => null);
const settings = await fetchSettingsSS();
const persona: Persona = constructMiniFiedPersona(
chatSession?.persona_icon_color ?? null,
chatSession?.persona_icon_shape ?? null,
chatSession?.persona_name ?? "",
chatSession?.persona_id ?? 0
backendChatSession?.persona_icon_color ?? null,
backendChatSession?.persona_icon_shape ?? null,
backendChatSession?.persona_name ?? "",
backendChatSession?.persona_id ?? 0
);
const chatSession = backendChatSession
? toChatSession(backendChatSession)
: null;
return <SharedChatDisplay chatSession={chatSession} persona={persona} />;
const appPageProps = {
settings,
chatSession,
};
return (
<AppPage {...appPageProps}>
<SharedChatDisplay chatSession={backendChatSession} persona={persona} />
</AppPage>
);
}

View File

@@ -8,9 +8,6 @@ import {
ValidSources,
} from "@/lib/types";
import { ChatSession, InputPrompt } from "@/app/chat/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
import { Settings } from "@/app/admin/settings/interfaces";
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { cookies, headers } from "next/headers";

View File

@@ -0,0 +1,30 @@
import {
BackendChatSession,
ChatSession,
toChatSession,
} from "@/app/chat/interfaces";
import { fetchSS } from "@/lib/utilsSS";
export async function fetchBackendChatSessionSS(
chatSessionId: string,
shared?: boolean
): Promise<BackendChatSession | null> {
const url = `/chat/get-chat-session/${chatSessionId}?${
shared ? "is_shared=True" : ""
}`;
const response = await fetchSS(url);
if (!response.ok) return null;
return (await response.json()) as BackendChatSession;
}
export async function fetchChatSessionSS(
chatSessionId: string,
shared?: boolean
): Promise<ChatSession | null> {
const backendChatSession = await fetchBackendChatSessionSS(
chatSessionId,
shared
);
if (!backendChatSession) return null;
return toChatSession(backendChatSession);
}

View File

@@ -24,7 +24,7 @@ import {
import { DateRangePickerValue } from "@/components/dateRangeSelectors/AdminDateRangeSelector";
import { SourceMetadata } from "./search/interfaces";
import { parseLlmDescriptor } from "./llm/utils";
import { ChatSession } from "@/app/chat/interfaces";
import { ChatSession, Message } from "@/app/chat/interfaces";
import { AllUsersResponse } from "./types";
import { Credential } from "./connectors/credentials";
import { SettingsContext } from "@/components/settings/SettingsProvider";
@@ -43,6 +43,12 @@ import { usePathname, useSearchParams } from "next/navigation";
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
import { useLLMProviders } from "./hooks/useLLMProviders";
import { useChatContext } from "@/refresh-components/contexts/ChatContext";
import {
useCurrentMessageHistory,
useIsFetching,
useLoadingError,
useSubmittedMessage,
} from "@/app/chat/stores/useChatSessionStore";
export function useIsMounted() {
const [mounted, setMounted] = useState(false);
@@ -1276,3 +1282,33 @@ export function useSourcePreferences({
isSourceEnabled,
};
}
export interface ShowCenteredInput {
isFetchingChatMessages: boolean;
loadingError: string | null;
messageHistory: Message[];
submittedMessage: string;
showCenteredInput: boolean;
}
// Determine whether to show the centered input (no messages yet)
export function useShowCenteredInput(): ShowCenteredInput {
const isFetchingChatMessages = useIsFetching();
const loadingError = useLoadingError();
const messageHistory = useCurrentMessageHistory();
const submittedMessage = useSubmittedMessage();
const showCenteredInput =
messageHistory.length === 0 &&
!isFetchingChatMessages &&
!loadingError &&
!submittedMessage;
return {
isFetchingChatMessages,
loadingError,
messageHistory,
submittedMessage,
showCenteredInput,
};
}

View File

@@ -19,7 +19,6 @@ import {
} from "@/lib/types";
import { useAssistantPreferences } from "@/app/chat/hooks/useAssistantPreferences";
import { useSession } from "@/app/chat/stores/useChatSessionStore";
import { BackendChatSession } from "@/app/chat/interfaces";
async function fetchAllAgents(): Promise<MinimalPersonaSnapshot[]> {
try {

View File

@@ -1,61 +1,66 @@
"use client";
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 Button from "@/refresh-components/buttons/Button";
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 { CombinedSettings } from "@/app/admin/settings/interfaces";
import { ChatSession } from "@/app/chat/interfaces";
import { useShowCenteredInput } from "@/lib/hooks";
export interface AppPageProps extends React.HtmlHTMLAttributes<HTMLDivElement> {
settings: CombinedSettings | null;
chatSession: ChatSession | null;
}
export default function AppPage({
settings,
chatSession,
export default function AppLayout({
className,
children,
...rest
}: React.HtmlHTMLAttributes<HTMLDivElement>) {
const settings = useSettingsContext();
}: AppPageProps) {
const customHeaderContent =
settings.enterpriseSettings?.custom_header_content;
settings?.enterpriseSettings?.custom_header_content;
const customFooterContent =
settings.enterpriseSettings?.custom_lower_disclaimer_content;
const customLogo = settings.enterpriseSettings?.use_custom_logo;
const appFocus = useAppFocus();
const { chatSessions } = useChatContext();
settings?.enterpriseSettings?.custom_lower_disclaimer_content;
const customLogo = settings?.enterpriseSettings?.use_custom_logo;
const [showShareModal, setShowShareModal] = useState(false);
const currentChatSession =
typeof appFocus === "object" && appFocus.type === "chat"
? chatSessions.find((session) => session.id === appFocus.id)
: undefined;
const { showCenteredInput } = useShowCenteredInput();
return (
<>
{showShareModal && currentChatSession && (
{chatSession && showShareModal && (
<ShareChatSessionModal
chatSession={currentChatSession}
chatSession={chatSession}
onClose={() => setShowShareModal(false)}
/>
)}
<div className="flex flex-col h-full w-full">
{/* Header */}
{(customHeaderContent || currentChatSession) && (
{(customHeaderContent || !showCenteredInput) && (
<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>
<Text text03 className={cn(!customHeaderContent && "invisible")}>
{customHeaderContent}
</Text>
</div>
<div className="flex-1 flex flex-row items-center justify-end px-1">
<IconButton
icon={SvgShare}
<Button
rightIcon={SvgShare}
transient={showShareModal}
tertiary
onClick={() => setShowShareModal(true)}
/>
className={cn(showCenteredInput && !chatSession && "invisible")}
>
Share Chat
</Button>
</div>
</header>
)}

View File

@@ -1,57 +0,0 @@
import Button from "@/refresh-components/buttons/Button";
import DefaultModalLayout, {
ModalProps,
} from "@/refresh-components/layouts/DefaultModalLayout";
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
import { useModal } from "@/refresh-components/contexts/ModalContext";
interface ProviderModalProps extends ModalProps {
// Footer props
onSubmit?: () => void;
submitDisabled?: boolean;
isSubmitting?: boolean;
submitLabel?: string;
cancelLabel?: string;
}
export default function ProviderModalLayout({
onSubmit,
submitDisabled = false,
isSubmitting = false,
submitLabel = "Connect",
cancelLabel = "Cancel",
children,
...rest
}: ProviderModalProps) {
const modal = useModal();
return (
<DefaultModalLayout {...rest}>
<div className="flex flex-col h-full max-h-[calc(100dvh-9rem)]">
<div className="flex-1 overflow-scroll">{children}</div>
{onSubmit && (
<div className="sticky bottom-0">
<div className="flex justify-end gap-2 w-full p-4">
<Button
type="button"
secondary
onClick={() => modal.toggle(false)}
>
{cancelLabel}
</Button>
<Button
type="button"
onClick={onSubmit}
disabled={submitDisabled || isSubmitting}
leftIcon={isSubmitting ? SimpleLoader : undefined}
>
{submitLabel}
</Button>
</div>
</div>
)}
</div>
</DefaultModalLayout>
);
}