Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ and this project adheres to

### Added

- ✨(sources) add source panel
- ✨(onboarding) add onboarding modal
- ✨(back) add ODT parsing support
- ✨(back) add self-documentation tool
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Message, SourceUIPart } from '@ai-sdk/ui-utils';
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
import { InfiniteData, useQueryClient } from '@tanstack/react-query';
import 'katex/dist/katex.min.css'; // `rehype-katex` does not import the CSS for you
import { useRouter } from 'next/router';
Expand All @@ -11,10 +11,11 @@ import React, {
useState,
} from 'react';
import type { ChangeEvent, FormEvent } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';

import { APIError, errorCauses, fetchAPI } from '@/api';
import { Box, Icon, Loader, Text } from '@/components';
import { Box, HorizontalSeparator, Icon, Loader, Text } from '@/components';
import { useConfig } from '@/core';
import { useProjectAttachments } from '@/features/attachments/api/useProjectAttachments';
import { useReindexProjectAttachment } from '@/features/attachments/api/useReindexProjectAttachment';
Expand Down Expand Up @@ -42,7 +43,9 @@ import {
STATUS_LINK_KINDS,
getReindexErrorMessage,
} from '@/features/chat/components/reindexErrorMessages';
import { SourceItemList } from '@/features/chat/components/SourceItemList';
import { useClipboard } from '@/hook';
import { useSourcePanelAnchor } from '@/features/sources-panel';
import { useResponsiveStore } from '@/stores';

import { useSourceMetadataCache } from '../hooks';
Expand Down Expand Up @@ -82,7 +85,8 @@ export const Chat = ({
const { data: config } = useConfig();
const statusPageUrl = config?.STATUS_PAGE_URL;
const copyToClipboard = useClipboard();
const { isMobile } = useResponsiveStore();
const { isMobile, isDesktop } = useResponsiveStore();
const sourcesPanelAnchorEl = useSourcePanelAnchor();

const streamProtocol = 'data'; // or 'text'

Expand All @@ -91,6 +95,7 @@ export const Chat = ({
toggleForceWebSearch,
selectedModelHrid,
setSelectedModelHrid,
setSourcesPanelOpen,
} = useChatPreferencesStore();

const { data: llmConfig } = useLLMConfiguration();
Expand Down Expand Up @@ -386,11 +391,84 @@ export const Chat = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages]);

const openSources = useCallback((messageId: string) => {
// Source-parts guard is handled at the call site (MessageItem only shows the button when sourceParts.length > 0),
// so we just toggle it here.
setIsSourceOpen((prev) => (prev === messageId ? null : messageId));
}, []);
const openSources = useCallback(
(messageId: string) => {
setIsSourceOpen((prev) => {
const next = prev === messageId ? null : messageId;
setSourcesPanelOpen(next !== null);
return next;
});
},
[setSourcesPanelOpen],
);

useEffect(() => {
setIsSourceOpen(null);
setSourcesPanelOpen(false);
}, [initialConversationId, setSourcesPanelOpen]);

const selectedSourceParts = useMemo(() => {
if (!isSourceOpen) {
return [];
}
const sourceMessage = messages.find(
(message) => message.id === isSourceOpen,
);
if (!sourceMessage?.parts) {
return [];
}
return sourceMessage.parts.filter(
(part): part is SourceUIPart => part.type === 'source',
);
}, [isSourceOpen, messages]);

const isSourcesPanelOpen =
Boolean(isSourceOpen) && selectedSourceParts.length > 0;

const sourcesPanelContent = isSourcesPanelOpen ? (
<Box
$direction="column"
$height="100%"
$css={`
background: var(--c--contextuals--background--surface--secondary);
border-left: 1px solid
var(--c--contextuals--border--surface--primary);
overflow-y: auto;
${!isDesktop ? 'border-left: none;' : ''}
`}
>
<Box
$direction="row"
$justify="space-between"
$align="center"
$margin={{ all: 'xs', left: 'sm' }}
>
<Text as="h2" $size="lg" $weight="700">
{selectedSourceParts.length}{' '}
{selectedSourceParts.length > 1 ? t('Sources') : t('Source')}
</Text>
<Button
color="neutral"
variant="tertiary"
onClick={() => {
setIsSourceOpen(null);
setSourcesPanelOpen(false);
}}
aria-label={t('Close sources panel')}
icon={
<Icon
iconName="close"
$theme="neutral"
$variation="550"
$size="18px"
/>
}
/>
</Box>
<HorizontalSeparator $withPadding={false} />
<SourceItemList parts={selectedSourceParts} getMetadata={getMetadata} />
</Box>
) : null;

// Memoize the last assistant message index to avoid recalculating in render
const lastAssistantMessageIndex = useMemo(() => {
Expand Down Expand Up @@ -926,6 +1004,7 @@ export const Chat = ({
height: 100%;
flex-grow: 1;
z-index: 1;
position: relative;
`}
>
<Box
Expand Down Expand Up @@ -1091,6 +1170,9 @@ export const Chat = ({
>
{chatErrorModal?.message}
</Modal>
{sourcesPanelContent &&
sourcesPanelAnchorEl &&
createPortal(sourcesPanelContent, sourcesPanelAnchorEl)}
</Box>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { Button } from '@gouvfr-lasuite/cunningham-react';
import React from 'react';
import { useTranslation } from 'react-i18next';

import { Box, Icon, Loader, Text } from '@/components';
import CheckmarkIcon from '@/assets/icons/uikit-custom/checkmark.svg';
import ClipboardIcon from '@/assets/icons/uikit-custom/clipboard.svg';
import SourcesIcon from '@/assets/icons/uikit-custom/sources.svg';
import { Box, Loader, Text } from '@/components';
import { useConfig } from '@/core/config';
import { AttachmentList } from '@/features/chat/components/AttachmentList';
import { FeedbackButtons } from '@/features/chat/components/FeedbackButtons';
Expand All @@ -13,14 +16,25 @@ import {
} from '@/features/chat/components/MessageBlock';
import { MessageEnergyIndicator } from '@/features/chat/components/MessageEnergyIndicator';
import { MoreActionsButton } from '@/features/chat/components/MoreActionsButton';
import { SourceItemList } from '@/features/chat/components/SourceItemList';
import { SummarizationError } from '@/features/chat/components/SummarizationError';
import { SummarizationProgress } from '@/features/chat/components/SummarizationProgress';
import { ToolInvocationItem } from '@/features/chat/components/ToolInvocationItem';
import { getMessageCo2Impact } from '@/features/chat/utils/getMessageCo2Impact';

import { ChatErrorType } from './ChatError';

const chatActionIconProps = {
width: 16,
height: 16,
color: 'var(--c--contextuals--content--semantic--neutral--secondary)',
className: 'action-chat-button-icon',
style: {
display: 'block',
fill: 'var(--c--contextuals--content--semantic--neutral--secondary)',
} as const,
'aria-hidden': true,
};

// Memoized blocks list to prevent parent re-renders from causing block remounts
const BlocksList = React.memo(
({ blocks, pending }: { blocks: string[]; pending: string }) => (
Expand Down Expand Up @@ -543,13 +557,11 @@ const MessageItemComponent: React.FC<MessageItemProps> = ({
onClick={handleCopy}
aria-label={isCopied ? t('Copied') : t('Copy')}
icon={
<Icon
iconName={isCopied ? 'check' : 'content_copy'}
$theme="neutral"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
isCopied ? (
<CheckmarkIcon {...chatActionIconProps} />
) : (
<ClipboardIcon {...chatActionIconProps} />
)
}
className="c__button--neutral action-chat-button"
></Button>
Expand All @@ -568,23 +580,18 @@ const MessageItemComponent: React.FC<MessageItemProps> = ({
variant="tertiary"
color="neutral"
onClick={handleOpenSources}
icon={
<Icon
iconName="book"
$theme="neutral"
$variation="550"
$size="16px"
className="action-chat-button-icon"
/>
}
icon={<SourcesIcon {...chatActionIconProps} />}
className={`c__button--neutral action-chat-button ${
isSourceOpen === message.id
? 'action-chat-button--open'
: ''
}`}
>
<Text>
{t('Show')} {sourceParts.length}{' '}
<Text $theme="neutral" $variation="tertiary">
{isSourceOpen !== message.id ? t('Show') : t('Hidden')}{' '}
{isSourceOpen !== message.id
? `${sourceParts.length} `
: ''}
{sourceParts.length !== 1 ? t('sources') : t('source')}
Comment on lines +591 to 595

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an action label instead of state text for the toggle button.

On Line 457, Hidden reads like a status, not a button action. Prefer an imperative label (Hide) when the panel is open.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 458-458: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=suitenumerique_conversations&issues=AZ4_g4kaNqnL1Qk74aQy&open=AZ4_g4kaNqnL1Qk74aQy&pullRequest=480


[warning] 457-457: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=suitenumerique_conversations&issues=AZ4_g4kaNqnL1Qk74aQx&open=AZ4_g4kaNqnL1Qk74aQx&pullRequest=480

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/apps/conversations/src/features/chat/components/MessageItem.tsx`
around lines 457 - 461, The toggle button currently shows a state label
("Hidden") instead of an action; update the JSX in MessageItem (the expression
using isSourceOpen, message.id and sourceParts) so when the panel is open
(isSourceOpen === message.id) it renders the imperative label t('Hide') instead
of t('Hidden'), and when closed it remains t('Show'); keep the count and
pluralization logic (sourceParts.length and t('source'/'sources')) unchanged.

</Text>
</Button>
Expand All @@ -603,16 +610,6 @@ const MessageItemComponent: React.FC<MessageItemProps> = ({
</Box>
</Box>
)}

{isSourceOpen === message.id && sourceParts.length > 0 && (
<Box
$css={`
animation: fade-in 0.2s ease-out;
`}
>
<SourceItemList parts={sourceParts} getMetadata={getMetadata} />
</Box>
)}
</Box>
</Box>
</Box>
Expand All @@ -632,6 +629,9 @@ const getToolInvocationStates = (message: Message): string =>
.map((part) => part.toolInvocation.state)
.join(',');

const getSourcePartsCount = (message: Message): number =>
(message.parts ?? []).filter((part) => part.type === 'source').length;

// Custom comparison function for React.memo
// Only re-render when props that affect rendering change
const arePropsEqual = (
Expand Down Expand Up @@ -668,6 +668,12 @@ const arePropsEqual = (
) {
return false;
}
if (
getSourcePartsCount(prevProps.message) !==
getSourcePartsCount(nextProps.message)
) {
return false;
}

// Check attachments
const prevAttachmentsLength =
Expand Down
Loading
Loading