🐛(front) fix left panel status + fix scroll (#65)
fix scroll + fix left panel status + fix show sources
This commit is contained in:
@@ -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
-2
@@ -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
-2
@@ -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 (
|
||||
<>
|
||||
|
||||
+3
-4
@@ -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'}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user