1
0
Fork 0

fix(sidepanel): Fixes some issues while conversation is streaming (#2828)

* fix(sidepanel): Stop navigation during streaming

* fix(sidepanel): Fix scroll jumping while conversation is streaming

* fix(sidepanel): Keep input enabled but disable sending while streaming

---------

Co-authored-by: Dylan Tientcheu <dylan.tientcheu@algolia.com>
This commit is contained in:
Paul Jankowski 2026-01-08 17:36:16 -05:00 committed by GitHub
parent 8c750e5102
commit 8ad60d9bab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 43 additions and 10 deletions

View file

@ -281,6 +281,19 @@ html[data-theme='dark'] {
display: none;
}
.DocSearch-Sidepanel-Action-menu[aria-disabled='true'],
.DocSearch-Sidepanel-Action-back[aria-disabled='true'] {
user-select: none;
pointer-events: none;
color: var(--docsearch-sidepanel-accent-muted);
}
.DocSearch-Sidepanel-Action-menu[aria-disabled='true']:hover,
.DocSearch-Sidepanel-Action-back[aria-disabled='true']:hover {
color: var(--docsearch-sidepanel-accent-muted);
background-color: transparent;
}
@media screen and (min-width: 769px) {
.DocSearch-Sidepanel-Action-menu,
.DocSearch-Sidepanel-Action-expand {

View file

@ -37,15 +37,22 @@ export function Menu({ children }: PropsWithChildren): JSX.Element {
type MenuTriggerProps = ButtonHTMLAttributes<HTMLButtonElement>;
function MenuTrigger({ children, className = '' }: PropsWithChildren<MenuTriggerProps>): JSX.Element {
function MenuTrigger({ children, className = '', disabled }: PropsWithChildren<MenuTriggerProps>): JSX.Element {
const { open, setOpen } = React.useContext(MenuContext);
function toggleOpen(): void {
if (disabled) return;
setOpen(!open);
}
return (
<button type="button" className={`DocSearch-Menu-trigger ${className}`} onClick={() => toggleOpen()}>
<button
type="button"
className={`DocSearch-Menu-trigger ${className}${disabled ? ' disabled' : ''}`}
aria-disabled={disabled}
onClick={() => toggleOpen()}
>
{children}
</button>
);

View file

@ -260,15 +260,17 @@ export const ConversationScreen = memo(
translations;
const mostRecentExchangeRef = React.useRef<HTMLDivElement>(null);
const totalExchanges = exchanges.length;
// Only scroll the most recent exchange into view when needed
React.useEffect(() => {
if (mostRecentExchangeRef.current) {
mostRecentExchangeRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
block: 'start',
});
}
}, [exchanges, props.status]);
}, [totalExchanges]);
return (
<div className="DocSearch-Sidepanel-ConversationScreen">

View file

@ -74,6 +74,8 @@ export const PromptForm = React.forwardRef<HTMLTextAreaElement, Props>(
};
const handleSend = (): void => {
if (isStreaming) return;
const prompt = userPrompt.trim();
if (prompt === '') return;
@ -90,6 +92,9 @@ export const PromptForm = React.forwardRef<HTMLTextAreaElement, Props>(
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>): void => {
// Allow Enter to work normally (new line) when streaming
if (isStreaming) return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@ -116,6 +121,8 @@ export const PromptForm = React.forwardRef<HTMLTextAreaElement, Props>(
onSubmit={(e) => {
e.preventDefault();
if (isStreaming) return;
handleSend();
}}
>
@ -123,7 +130,6 @@ export const PromptForm = React.forwardRef<HTMLTextAreaElement, Props>(
ref={promptRef}
placeholder={promptPlaceholder}
className="DocSearch-Sidepanel-Prompt--textarea"
aria-disabled={isStreaming}
value={userPrompt}
aria-label={promptAriaLabelText}
aria-labelledby="prompt-label"

View file

@ -363,6 +363,7 @@ function SidepanelInner(
exchanges={exchanges}
setSidepanelState={setSidepanelState}
hasConversations={conversations.getAll().length > 0}
isStreaming={isStreaming}
onNewConversation={handleStartNewConversation}
onToggleExpanded={toggleIsExpanded}
onClose={onClose}

View file

@ -21,15 +21,17 @@ import type { SidepanelState } from './types';
type BackButtonProps = {
mobile?: boolean;
onBack: () => void;
disabled?: boolean;
};
const BackButton = ({ onBack, mobile = false }: BackButtonProps): JSX.Element => {
const BackButton = ({ onBack, mobile = false, disabled = false }: BackButtonProps): JSX.Element => {
return (
<button
type="button"
className={`DocSearch-Action DocSearch-Sidepanel-Action-back${mobile ? ' mobile' : ''}`}
title="Go back to previous screen"
onClick={onBack}
aria-disabled={disabled}
onClick={disabled ? undefined : onBack}
>
<BackIcon />
</button>
@ -64,6 +66,7 @@ type SidepanelHeaderProps = {
onClose: () => void;
translations?: HeaderTranslations;
hasConversations: boolean;
isStreaming: boolean;
};
export const SidepanelHeader = React.memo(
@ -76,6 +79,7 @@ export const SidepanelHeader = React.memo(
onClose,
translations = {},
hasConversations,
isStreaming,
}: SidepanelHeaderProps): JSX.Element => {
const {
title = 'Ask AI',
@ -113,7 +117,7 @@ export const SidepanelHeader = React.memo(
return (
<header className="DocSearch-Sidepanel-Header">
<div className="DocSearch-Sidepanel-Header--left">
<BackButton mobile={true} onBack={onBack} />
<BackButton mobile={true} disabled={isStreaming} onBack={onBack} />
{sidepanelState === 'conversation-history' && <div className="DocSearch-Divider" />}
@ -133,14 +137,14 @@ export const SidepanelHeader = React.memo(
</button>
</div>
<div className="DocSearch-Sidepanel-Header--center">
<BackButton onBack={onBack} />
<BackButton disabled={isStreaming} onBack={onBack} />
<SparklesIcon className="DocSearch-Sidepanel-Header-TitleIcon" />
<h2 className="DocSearch-Sidepanel-Title">{header}</h2>
</div>
<div className="DocSearch-Sidepanel-Header--right">
{sidepanelState !== 'conversation-history' && (!newConversationDisabled || hasConversations) && (
<Menu>
<Menu.Trigger className="DocSearch-Action DocSearch-Sidepanel-Action-menu">
<Menu.Trigger className="DocSearch-Action DocSearch-Sidepanel-Action-menu" disabled={isStreaming}>
<MoreVerticalIcon />
</Menu.Trigger>
<Menu.Content>