🐛(front) fix left panel status + fix scroll (#65)

fix scroll + fix left panel status + fix show sources
This commit is contained in:
elvoisin
2025-10-14 10:05:28 +02:00
committed by GitHub
parent 7c6e3da2d9
commit 146c9ff19f
9 changed files with 116 additions and 94 deletions
+1
View File
@@ -8,6 +8,7 @@ and this project adheres to
## [Unreleased]
🐛(front) fix left panel status + fix scroll
-🐛(llm) add is_active field and persist chat preference
### Changed
@@ -119,7 +119,6 @@ export const Chat = ({
setChatContainerRef(chatContainerRef);
}, []);
// Détecter si l'utilisateur scroll vers le haut
useEffect(() => {
const container = chatContainerRef.current;
if (!container) {
@@ -127,6 +126,11 @@ export const Chat = ({
}
const handleScroll = () => {
// Ignorer les scrolls automatiques
if (isAutoScrollingRef.current) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = container;
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
@@ -154,6 +158,7 @@ export const Chat = ({
number | null
>(null);
const [userScrolledUp, setUserScrolledUp] = useState(false);
const isAutoScrollingRef = useRef(false);
const { mutate: createChatConversation } = useCreateChatConversation();
@@ -195,10 +200,14 @@ export const Chat = ({
// Scroll to bottom when new messages arrive
const scrollToBottom = useCallback(() => {
if (chatContainerRef.current) {
isAutoScrollingRef.current = true;
chatContainerRef.current.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: hasInitialized ? 'smooth' : 'auto',
});
setTimeout(() => {
isAutoScrollingRef.current = false;
}, 500);
}
}, [hasInitialized]);
@@ -314,10 +323,14 @@ export const Chat = ({
if (lastUserMessageElement) {
const messageTop = (lastUserMessageElement as HTMLElement)
.offsetTop;
isAutoScrollingRef.current = true;
chatContainerRef.current.scrollTo({
top: messageTop,
behavior: 'smooth',
});
setTimeout(() => {
isAutoScrollingRef.current = false;
}, 500);
}
}
}
@@ -394,10 +407,14 @@ export const Chat = ({
setTimeout(() => {
if (chatContainerRef.current) {
isAutoScrollingRef.current = true;
chatContainerRef.current.scrollTo({
top: chatContainerRef.current.scrollHeight,
behavior: 'auto',
});
setTimeout(() => {
isAutoScrollingRef.current = false;
}, 100);
}
}, 100);
}
@@ -638,7 +655,7 @@ export const Chat = ({
) : null,
)}
</Box>
{message.role !== 'user' && !isCurrentlyStreaming && (
{message.role !== 'user' && status !== 'streaming' && (
<Box
$css="color: #222631; font-size: 12px;"
$direction="row"
@@ -647,78 +664,80 @@ export const Chat = ({
$gap="6px"
$margin={{ top: 'base' }}
>
<Box
$direction="row"
$align="center"
$gap="4px"
className="c__button--neutral action-chat-button"
onClick={() => copyToClipboard(message.content)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyToClipboard(message.content);
}
}}
role="button"
tabIndex={0}
>
<Icon
iconName="content_copy"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
{!isMobile && (
<Text $theme="greyscale" $variation="550">
{t('Copy')}
</Text>
)}
</Box>
{message.parts?.some(
(part) => part.type === 'source',
) &&
(() => {
const sourceCount =
message.parts?.filter(
(part) => part.type === 'source',
).length || 0;
return (
<Box
$direction="row"
$align="center"
$gap="4px"
className={`c__button--neutral action-chat-button ${isSourceOpen ? 'action-chat-button--open' : ''}`}
onClick={() => openSources(message.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openSources(message.id);
}
}}
role="button"
tabIndex={0}
>
<Icon
iconName="book"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
<Text
$theme="greyscale"
$variation="550"
$weight="500"
$size="12px"
<Box $direction="row" $gap="4px">
<Box
$direction="row"
$align="center"
$gap="4px"
className="c__button--neutral action-chat-button"
onClick={() => copyToClipboard(message.content)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
copyToClipboard(message.content);
}
}}
role="button"
tabIndex={0}
>
<Icon
iconName="content_copy"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
{!isMobile && (
<Text $theme="greyscale" $variation="550">
{t('Copy')}
</Text>
)}
</Box>
{message.parts?.some(
(part) => part.type === 'source',
) &&
(() => {
const sourceCount =
message.parts?.filter(
(part) => part.type === 'source',
).length || 0;
return (
<Box
$direction="row"
$align="center"
$gap="4px"
className={`c__button--neutral action-chat-button ${isSourceOpen ? 'action-chat-button--open' : ''}`}
onClick={() => openSources(message.id)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openSources(message.id);
}
}}
role="button"
tabIndex={0}
>
{t('Show') +
` ${sourceCount} ` +
t('sources')}
</Text>
</Box>
);
})()}
<Icon
iconName="book"
$theme="greyscale"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
<Text
$theme="greyscale"
$variation="550"
$weight="500"
$size="12px"
>
{t('Show') +
` ${sourceCount} ` +
t('sources')}
</Text>
</Box>
);
})()}
</Box>
<Box $direction="row" $gap="4px">
{/* We should display the button, but disabled if no trace linked */}
{conversationId &&
@@ -4,8 +4,11 @@ import { persist } from 'zustand/middleware';
interface ChatPreferencesState {
selectedModelHrid: string | null;
forceWebSearch: boolean;
isPanelOpen: boolean;
setSelectedModelHrid: (hrid: string | null) => void;
toggleForceWebSearch: () => void;
setPanelOpen: (isOpen: boolean) => void;
togglePanel: () => void;
}
export const useChatPreferencesStore = create<ChatPreferencesState>()(
@@ -13,9 +16,12 @@ export const useChatPreferencesStore = create<ChatPreferencesState>()(
(set) => ({
selectedModelHrid: null,
forceWebSearch: false,
isPanelOpen: false,
setSelectedModelHrid: (hrid) => set({ selectedModelHrid: hrid }),
toggleForceWebSearch: () =>
set((state) => ({ forceWebSearch: !state.forceWebSearch })),
setPanelOpen: (isOpen) => set({ isPanelOpen: isOpen }),
togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })),
}),
{
name: 'chat-preferences',
@@ -2,11 +2,11 @@ import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import LeftPanelIcon from '@/assets/icons/left-panel-bold.svg';
import { useLeftPanelStore } from '@/features/left-panel';
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
export const ButtonToggleLeftPanel = () => {
const { t } = useTranslation();
const { isPanelOpen: _isPanelOpen, togglePanel } = useLeftPanelStore();
const { togglePanel } = useChatPreferencesStore();
return (
<Button
@@ -2,11 +2,11 @@ import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Icon } from '@/components/';
import { useLeftPanelStore } from '@/features/left-panel';
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
export const ButtonTogglePanel = () => {
const { t } = useTranslation();
const { isPanelOpen, togglePanel } = useLeftPanelStore();
const { isPanelOpen, togglePanel } = useChatPreferencesStore();
return (
<Button
@@ -11,9 +11,9 @@ import { productName } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { ButtonLogin } from '@/features/auth';
import { useChatScroll } from '@/features/chat/hooks';
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
import { Feedback } from '@/features/feedback/Feedback';
import { LanguagePicker } from '@/features/language';
import { useLeftPanelStore } from '@/features/left-panel/stores';
import { useResponsiveStore } from '@/stores';
import { HEADER_HEIGHT } from '../conf';
@@ -26,7 +26,7 @@ export const Header = () => {
const { t } = useTranslation();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { togglePanel } = useLeftPanelStore();
const { setPanelOpen } = useChatPreferencesStore();
const { isAtTop } = useChatScroll();
const router = useRouter();
@@ -75,7 +75,7 @@ export const Header = () => {
size="medium"
onClick={() => {
router.push('/');
togglePanel(false);
setPanelOpen(false);
}}
className="mobile-no-focus"
aria-label={t('New chat')}
@@ -1,16 +1,14 @@
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { createGlobalStyle, css } from 'styled-components';
import { Box, SeparatedSection } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { ButtonLogin } from '@/features/auth';
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
import { LanguagePicker } from '@/features/language';
import { SettingsButton } from '@/features/settings';
import { useResponsiveStore } from '@/stores';
import { useLeftPanelStore } from '../stores';
import { LeftPanelContent } from './LeftPanelContent';
import { LeftPanelHeader } from './LeftPanelHeader';
@@ -24,13 +22,12 @@ export const LeftPanel = () => {
const { isDesktop } = useResponsiveStore();
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const { togglePanel, isPanelOpen } = useLeftPanelStore();
const pathname = usePathname();
const { setPanelOpen, isPanelOpen } = useChatPreferencesStore();
useEffect(() => {
togglePanel(isDesktop ? true : false);
}, [pathname, togglePanel, isDesktop]);
setPanelOpen(isDesktop ? true : false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDesktop]);
return (
<>
@@ -6,22 +6,21 @@ import { PropsWithChildren } from 'react';
import NewChatIcon from '@/assets/icons/new-message-bold.svg';
import { Box, SeparatedSection } from '@/components';
import { useAuth } from '@/features/auth';
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
import { SettingsButton } from '@/features/settings';
import { useResponsiveStore } from '@/stores';
import { useLeftPanelStore } from '../stores';
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
const router = useRouter();
const { authenticated } = useAuth();
const { isDesktop } = useResponsiveStore();
const { togglePanel } = useLeftPanelStore();
const { setPanelOpen } = useChatPreferencesStore();
const goToHome = () => {
router.push('/');
if (!isDesktop) {
togglePanel(false);
setPanelOpen(false);
}
};
@@ -3,9 +3,9 @@ import { css } from 'styled-components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore';
import { Header } from '@/features/header';
import { LeftPanel } from '@/features/left-panel';
import { useLeftPanelStore } from '@/features/left-panel/stores';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { useResponsiveStore } from '@/stores';
@@ -19,7 +19,7 @@ export function MainLayout({
}: PropsWithChildren<MainLayoutProps>) {
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const { togglePanel: _togglePanel, isPanelOpen } = useLeftPanelStore();
const { isPanelOpen } = useChatPreferencesStore();
const HEADER_HEIGHT = `${isDesktop ? '65' : '52'}`;