mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
refactor(chat): introduce ChatViewport and NestedScroll components for improved chat UI structure and functionality
This commit is contained in:
parent
af66fbf106
commit
a66c1576b9
7 changed files with 99 additions and 156 deletions
44
surfsense_web/components/assistant-ui/chat-viewport.tsx
Normal file
44
surfsense_web/components/assistant-ui/chat-viewport.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ThreadPrimitive } from "@assistant-ui/react";
|
||||||
|
import { ArrowDownIcon } from "lucide-react";
|
||||||
|
import type { FC, ReactNode } from "react";
|
||||||
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
|
|
||||||
|
const ChatScrollToBottom: FC = () => (
|
||||||
|
<ThreadPrimitive.ScrollToBottom asChild>
|
||||||
|
<TooltipIconButton
|
||||||
|
tooltip="Scroll to bottom"
|
||||||
|
variant="outline"
|
||||||
|
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
|
||||||
|
>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</TooltipIconButton>
|
||||||
|
</ThreadPrimitive.ScrollToBottom>
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ChatViewportProps {
|
||||||
|
children: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatViewport: FC<ChatViewportProps> = ({ children, footer }) => (
|
||||||
|
<ThreadPrimitive.Viewport
|
||||||
|
scrollToBottomOnRunStart
|
||||||
|
scrollToBottomOnInitialize
|
||||||
|
scrollToBottomOnThreadSwitch
|
||||||
|
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
|
||||||
|
style={{ scrollbarGutter: "stable" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{footer ? (
|
||||||
|
<ThreadPrimitive.ViewportFooter
|
||||||
|
className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-main-panel pb-4 md:pb-6"
|
||||||
|
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
|
||||||
|
>
|
||||||
|
<ChatScrollToBottom />
|
||||||
|
{footer}
|
||||||
|
</ThreadPrimitive.ViewportFooter>
|
||||||
|
) : null}
|
||||||
|
</ThreadPrimitive.Viewport>
|
||||||
|
);
|
||||||
24
surfsense_web/components/assistant-ui/nested-scroll.tsx
Normal file
24
surfsense_web/components/assistant-ui/nested-scroll.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { forwardRef, type ComponentPropsWithoutRef, type WheelEvent } from "react";
|
||||||
|
|
||||||
|
export type NestedScrollProps = ComponentPropsWithoutRef<"div">;
|
||||||
|
|
||||||
|
export const NestedScroll = forwardRef<HTMLDivElement, NestedScrollProps>(
|
||||||
|
({ onWheel, ...props }, ref) => {
|
||||||
|
const handleWheel = (event: WheelEvent<HTMLDivElement>) => {
|
||||||
|
const el = event.currentTarget;
|
||||||
|
const canScrollUp = el.scrollTop > 0;
|
||||||
|
const canScrollDown = el.scrollTop < el.scrollHeight - el.clientHeight - 1;
|
||||||
|
const goingUp = event.deltaY < 0;
|
||||||
|
const goingDown = event.deltaY > 0;
|
||||||
|
if ((goingUp && canScrollUp) || (goingDown && canScrollDown)) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
onWheel?.(event);
|
||||||
|
};
|
||||||
|
return <div ref={ref} onWheel={handleWheel} {...props} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
NestedScroll.displayName = "NestedScroll";
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { ThreadPrimitive } from "@assistant-ui/react";
|
|
||||||
import { ArrowDownIcon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
||||||
|
|
||||||
export const ThreadScrollToBottom: FC = () => {
|
|
||||||
return (
|
|
||||||
<ThreadPrimitive.ScrollToBottom asChild>
|
|
||||||
<TooltipIconButton
|
|
||||||
tooltip="Scroll to bottom"
|
|
||||||
variant="outline"
|
|
||||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
|
|
||||||
>
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</TooltipIconButton>
|
|
||||||
</ThreadPrimitive.ScrollToBottom>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -5,12 +5,10 @@ import {
|
||||||
ThreadPrimitive,
|
ThreadPrimitive,
|
||||||
useAui,
|
useAui,
|
||||||
useAuiState,
|
useAuiState,
|
||||||
useThreadViewportStore,
|
|
||||||
} from "@assistant-ui/react";
|
} from "@assistant-ui/react";
|
||||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
Camera,
|
Camera,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
|
@ -55,6 +53,7 @@ import {
|
||||||
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||||
import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
|
import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
|
||||||
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status";
|
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status";
|
||||||
|
import { ChatViewport } from "@/components/assistant-ui/chat-viewport";
|
||||||
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
|
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
|
||||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||||
import {
|
import {
|
||||||
|
|
@ -112,10 +111,17 @@ const ThreadContent: FC = () => {
|
||||||
["--thread-max-width" as string]: "44rem",
|
["--thread-max-width" as string]: "44rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThreadPrimitive.Viewport
|
<ChatViewport
|
||||||
turnAnchor="top"
|
footer={
|
||||||
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
|
<>
|
||||||
style={{ scrollbarGutter: "stable" }}
|
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
||||||
|
<PremiumQuotaPinnedAlert />
|
||||||
|
</AuiIf>
|
||||||
|
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
||||||
|
<Composer />
|
||||||
|
</AuiIf>
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AuiIf condition={({ thread }) => thread.isEmpty}>
|
<AuiIf condition={({ thread }) => thread.isEmpty}>
|
||||||
<ThreadWelcome />
|
<ThreadWelcome />
|
||||||
|
|
@ -128,24 +134,7 @@ const ThreadContent: FC = () => {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</ChatViewport>
|
||||||
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
|
||||||
<div className="grow" />
|
|
||||||
</AuiIf>
|
|
||||||
|
|
||||||
<ThreadPrimitive.ViewportFooter
|
|
||||||
className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-main-panel pb-4 md:pb-6"
|
|
||||||
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
|
|
||||||
>
|
|
||||||
<ThreadScrollToBottom />
|
|
||||||
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
|
||||||
<PremiumQuotaPinnedAlert />
|
|
||||||
</AuiIf>
|
|
||||||
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
|
||||||
<Composer />
|
|
||||||
</AuiIf>
|
|
||||||
</ThreadPrimitive.ViewportFooter>
|
|
||||||
</ThreadPrimitive.Viewport>
|
|
||||||
</ThreadPrimitive.Root>
|
</ThreadPrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -181,20 +170,6 @@ const PremiumQuotaPinnedAlert: FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThreadScrollToBottom: FC = () => {
|
|
||||||
return (
|
|
||||||
<ThreadPrimitive.ScrollToBottom asChild>
|
|
||||||
<TooltipIconButton
|
|
||||||
tooltip="Scroll to bottom"
|
|
||||||
variant="outline"
|
|
||||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
|
|
||||||
>
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</TooltipIconButton>
|
|
||||||
</ThreadPrimitive.ScrollToBottom>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTimeBasedGreeting = (user?: { display_name?: string | null; email?: string }): string => {
|
const getTimeBasedGreeting = (user?: { display_name?: string | null; email?: string }): string => {
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
|
|
||||||
|
|
@ -411,23 +386,9 @@ const Composer: FC = () => {
|
||||||
>(new Map());
|
>(new Map());
|
||||||
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
const documentPickerRef = useRef<DocumentMentionPickerRef>(null);
|
||||||
const promptPickerRef = useRef<PromptPickerRef>(null);
|
const promptPickerRef = useRef<PromptPickerRef>(null);
|
||||||
const viewportRef = useRef<Element | null>(null);
|
|
||||||
const { search_space_id, chat_id } = useParams();
|
const { search_space_id, chat_id } = useParams();
|
||||||
const aui = useAui();
|
const aui = useAui();
|
||||||
const threadViewportStore = useThreadViewportStore();
|
|
||||||
const hasAutoFocusedRef = useRef(false);
|
const hasAutoFocusedRef = useRef(false);
|
||||||
const submitCleanupRef = useRef<(() => void) | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
submitCleanupRef.current?.();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Store viewport element reference on mount
|
|
||||||
useEffect(() => {
|
|
||||||
viewportRef.current = document.querySelector(".aui-thread-viewport");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const electronAPI = useElectronAPI();
|
const electronAPI = useElectronAPI();
|
||||||
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
|
const [clipboardInitialText, setClipboardInitialText] = useState<string | undefined>();
|
||||||
|
|
@ -626,7 +587,6 @@ const Composer: FC = () => {
|
||||||
[showDocumentPopover, showPromptPicker]
|
[showDocumentPopover, showPromptPicker]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Submit message (blocked during streaming, document picker open, or AI responding to another user)
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
if (isThreadRunning || isBlockedByOtherUser) return;
|
if (isThreadRunning || isBlockedByOtherUser) return;
|
||||||
if (showDocumentPopover || showPromptPicker) return;
|
if (showDocumentPopover || showPromptPicker) return;
|
||||||
|
|
@ -638,50 +598,9 @@ const Composer: FC = () => {
|
||||||
setClipboardInitialText(undefined);
|
setClipboardInitialText(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportEl = viewportRef.current;
|
|
||||||
const heightBefore = viewportEl?.scrollHeight ?? 0;
|
|
||||||
|
|
||||||
aui.composer().send();
|
aui.composer().send();
|
||||||
editorRef.current?.clear();
|
editorRef.current?.clear();
|
||||||
setMentionedDocuments([]);
|
setMentionedDocuments([]);
|
||||||
|
|
||||||
// With turnAnchor="top", ViewportSlack adds min-height to the last
|
|
||||||
// assistant message so that scrolling-to-bottom actually positions the
|
|
||||||
// user message at the TOP of the viewport. That slack height is
|
|
||||||
// calculated asynchronously (ResizeObserver → style → layout).
|
|
||||||
// Poll via rAF for ~500ms, re-scrolling whenever scrollHeight changes.
|
|
||||||
const scrollToBottom = () =>
|
|
||||||
threadViewportStore.getState().scrollToBottom({ behavior: "instant" });
|
|
||||||
|
|
||||||
let lastHeight = heightBefore;
|
|
||||||
let frames = 0;
|
|
||||||
let cancelled = false;
|
|
||||||
const POLL_FRAMES = 30;
|
|
||||||
|
|
||||||
const pollAndScroll = () => {
|
|
||||||
if (cancelled) return;
|
|
||||||
const el = viewportRef.current;
|
|
||||||
if (el) {
|
|
||||||
const h = el.scrollHeight;
|
|
||||||
if (h !== lastHeight) {
|
|
||||||
lastHeight = h;
|
|
||||||
scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (++frames < POLL_FRAMES) {
|
|
||||||
requestAnimationFrame(pollAndScroll);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
requestAnimationFrame(pollAndScroll);
|
|
||||||
|
|
||||||
const t1 = setTimeout(scrollToBottom, 100);
|
|
||||||
const t2 = setTimeout(scrollToBottom, 300);
|
|
||||||
|
|
||||||
submitCleanupRef.current = () => {
|
|
||||||
cancelled = true;
|
|
||||||
clearTimeout(t1);
|
|
||||||
clearTimeout(t2);
|
|
||||||
};
|
|
||||||
}, [
|
}, [
|
||||||
showDocumentPopover,
|
showDocumentPopover,
|
||||||
showPromptPicker,
|
showPromptPicker,
|
||||||
|
|
@ -690,7 +609,6 @@ const Composer: FC = () => {
|
||||||
clipboardInitialText,
|
clipboardInitialText,
|
||||||
aui,
|
aui,
|
||||||
setMentionedDocuments,
|
setMentionedDocuments,
|
||||||
threadViewportStore,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleDocumentRemove = useCallback(
|
const handleDocumentRemove = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
isDoomLoopInterrupt,
|
isDoomLoopInterrupt,
|
||||||
} from "@/components/tool-ui/doom-loop-approval";
|
} from "@/components/tool-ui/doom-loop-approval";
|
||||||
import { GenericHitlApprovalToolUI } from "@/components/tool-ui/generic-hitl-approval";
|
import { GenericHitlApprovalToolUI } from "@/components/tool-ui/generic-hitl-approval";
|
||||||
|
import { NestedScroll } from "@/components/assistant-ui/nested-scroll";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -475,7 +476,7 @@ const DefaultToolFallbackInner: ToolCallMessagePartComponent = (props) => {
|
||||||
{(argsText || isRunning) && (
|
{(argsText || isRunning) && (
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Inputs</p>
|
<p className="text-xs font-medium text-muted-foreground">Inputs</p>
|
||||||
<div className="max-h-48 overflow-auto rounded-md bg-muted/40">
|
<NestedScroll className="max-h-48 overflow-auto rounded-md bg-muted/40">
|
||||||
{argsText ? (
|
{argsText ? (
|
||||||
<pre className="px-3 py-2 text-xs text-foreground/80 whitespace-pre-wrap break-all font-mono">
|
<pre className="px-3 py-2 text-xs text-foreground/80 whitespace-pre-wrap break-all font-mono">
|
||||||
{argsText}
|
{argsText}
|
||||||
|
|
@ -489,7 +490,7 @@ const DefaultToolFallbackInner: ToolCallMessagePartComponent = (props) => {
|
||||||
Waiting for input…
|
Waiting for input…
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</NestedScroll>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isCancelled && result !== undefined && (
|
{!isCancelled && result !== undefined && (
|
||||||
|
|
@ -497,11 +498,11 @@ const DefaultToolFallbackInner: ToolCallMessagePartComponent = (props) => {
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<p className="text-xs font-medium text-muted-foreground">Result</p>
|
<p className="text-xs font-medium text-muted-foreground">Result</p>
|
||||||
<div className="max-h-64 overflow-auto rounded-md bg-muted/40">
|
<NestedScroll className="max-h-64 overflow-auto rounded-md bg-muted/40">
|
||||||
<pre className="px-3 py-2 text-xs text-foreground/80 whitespace-pre-wrap break-all font-mono">
|
<pre className="px-3 py-2 text-xs text-foreground/80 whitespace-pre-wrap break-all font-mono">
|
||||||
{typeof result === "string" ? result : serializedResult}
|
{typeof result === "string" ? result : serializedResult}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</NestedScroll>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AuiIf, ThreadPrimitive } from "@assistant-ui/react";
|
import { AuiIf, ThreadPrimitive } from "@assistant-ui/react";
|
||||||
import { ArrowDownIcon } from "lucide-react";
|
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
|
import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
|
||||||
|
import { ChatViewport } from "@/components/assistant-ui/chat-viewport";
|
||||||
import { EditComposer } from "@/components/assistant-ui/edit-composer";
|
import { EditComposer } from "@/components/assistant-ui/edit-composer";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
||||||
import { UserMessage } from "@/components/assistant-ui/user-message";
|
import { UserMessage } from "@/components/assistant-ui/user-message";
|
||||||
import { FreeComposer } from "./free-composer";
|
import { FreeComposer } from "./free-composer";
|
||||||
|
|
||||||
|
|
@ -24,20 +23,6 @@ const FreeThreadWelcome: FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ThreadScrollToBottom: FC = () => {
|
|
||||||
return (
|
|
||||||
<ThreadPrimitive.ScrollToBottom asChild>
|
|
||||||
<TooltipIconButton
|
|
||||||
tooltip="Scroll to bottom"
|
|
||||||
variant="outline"
|
|
||||||
className="aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
|
|
||||||
>
|
|
||||||
<ArrowDownIcon />
|
|
||||||
</TooltipIconButton>
|
|
||||||
</ThreadPrimitive.ScrollToBottom>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FreeThread: FC = () => {
|
export const FreeThread: FC = () => {
|
||||||
return (
|
return (
|
||||||
<ThreadPrimitive.Root
|
<ThreadPrimitive.Root
|
||||||
|
|
@ -46,10 +31,12 @@ export const FreeThread: FC = () => {
|
||||||
["--thread-max-width" as string]: "44rem",
|
["--thread-max-width" as string]: "44rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThreadPrimitive.Viewport
|
<ChatViewport
|
||||||
turnAnchor="top"
|
footer={
|
||||||
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
|
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
||||||
style={{ scrollbarGutter: "stable" }}
|
<FreeComposer />
|
||||||
|
</AuiIf>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AuiIf condition={({ thread }) => thread.isEmpty}>
|
<AuiIf condition={({ thread }) => thread.isEmpty}>
|
||||||
<FreeThreadWelcome />
|
<FreeThreadWelcome />
|
||||||
|
|
@ -62,21 +49,7 @@ export const FreeThread: FC = () => {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</ChatViewport>
|
||||||
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
|
||||||
<div className="grow" />
|
|
||||||
</AuiIf>
|
|
||||||
|
|
||||||
<ThreadPrimitive.ViewportFooter
|
|
||||||
className="aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-main-panel pb-4 md:pb-6"
|
|
||||||
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
|
|
||||||
>
|
|
||||||
<ThreadScrollToBottom />
|
|
||||||
<AuiIf condition={({ thread }) => !thread.isEmpty}>
|
|
||||||
<FreeComposer />
|
|
||||||
</AuiIf>
|
|
||||||
</ThreadPrimitive.ViewportFooter>
|
|
||||||
</ThreadPrimitive.Viewport>
|
|
||||||
</ThreadPrimitive.Root>
|
</ThreadPrimitive.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,16 +45,17 @@ export const PublicThread: FC<PublicThreadProps> = ({ footer }) => {
|
||||||
["--thread-max-width" as string]: "44rem",
|
["--thread-max-width" as string]: "44rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ThreadPrimitive.Viewport className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4">
|
<ThreadPrimitive.Viewport
|
||||||
|
scrollToBottomOnInitialize
|
||||||
|
scrollToBottomOnThreadSwitch
|
||||||
|
className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4 pb-6"
|
||||||
|
>
|
||||||
<ThreadPrimitive.Messages
|
<ThreadPrimitive.Messages
|
||||||
components={{
|
components={{
|
||||||
UserMessage: PublicUserMessage,
|
UserMessage: PublicUserMessage,
|
||||||
AssistantMessage: PublicAssistantMessage,
|
AssistantMessage: PublicAssistantMessage,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Spacer to ensure footer doesn't overlap last message */}
|
|
||||||
<div className="h-24" />
|
|
||||||
</ThreadPrimitive.Viewport>
|
</ThreadPrimitive.Viewport>
|
||||||
|
|
||||||
{footer && (
|
{footer && (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue