🎨(front) global layout modification (#21)

delete unused languages + modify scroll down + change source item + attachements
This commit is contained in:
elvoisin
2025-09-02 11:39:26 +02:00
committed by GitHub
parent 22769af0e5
commit df3934367a
15 changed files with 285 additions and 218 deletions
+2 -1
View File
@@ -8,10 +8,11 @@ and this project adheres to
## [Unreleased]
- (front) global layout UI
- 🎨(front) global layout modification
### Changed
- ✨(front) global layout UI
- ♻️(chat) rewrite backend using Pydantic AI SDK #4
- 🗃️(chat) enforce messages stored JSON format #6
- 🐛(chat) UI messages must have a unique identifier #6
+3 -3
View File
@@ -134,9 +134,9 @@ class Base(Configuration):
(
("en-us", "English"),
("fr-fr", "Français"),
("de-de", "Deutsch"),
("nl-nl", "Nederlands"),
("es-es", "Español"),
# ("de-de", "Deutsch"),
# ("nl-nl", "Nederlands"),
# ("es-es", "Español"),
)
)
+3 -3
View File
@@ -117,9 +117,9 @@ class Migration(migrations.Migration):
choices=[
("en-us", "English"),
("fr-fr", "Français"),
("de-de", "Deutsch"),
("nl-nl", "Nederlands"),
("es-es", "Español"),
# ("de-de", "Deutsch"),
# ("nl-nl", "Nederlands"),
# ("es-es", "Español"),
],
default=None,
help_text="The language in which the user wants to see the interface.",
+3 -3
View File
@@ -51,9 +51,9 @@ def test_api_config(is_authenticated):
"LANGUAGES": [
["en-us", "English"],
["fr-fr", "Français"],
["de-de", "Deutsch"],
["nl-nl", "Nederlands"],
["es-es", "Español"],
# ["de-de", "Deutsch"],
# ["nl-nl", "Nederlands"],
# ["es-es", "Español"],
],
"LANGUAGE_CODE": "en-us",
"MEDIA_BASE_URL": "http://testserver/",
@@ -33,6 +33,7 @@ export const tokens = {
'greyscale-700': '#3A3A3A',
'greyscale-800': '#2A2A2A',
'greyscale-900': '#242424',
'greyscale-050': '#EEF1F4',
'greyscale-000': '#fff',
'primary-100': '#ECECFE',
'primary-200': '#E3E3FD',
@@ -77,7 +78,6 @@ export const tokens = {
'primary-050': '#F5F5FE',
'primary-150': '#F4F4FD',
'greyscale-text': '#303C4B',
'greyscale-050': '#F6F6F6',
'greyscale-250': '#ddd',
'greyscale-350': '#ddd',
'greyscale-750': '#353535',
@@ -17,9 +17,16 @@ const fetchAPIAdapter = (input: RequestInfo | URL, init?: RequestInit) => {
// Add force_web_search parameter if it's globally enabled
if ((window as { globalForceWebSearch?: boolean }).globalForceWebSearch) {
const urlObj = new URL(url);
urlObj.searchParams.set('force_web_search', 'true');
url = urlObj.toString();
// For relative URLs, just append the parameter
if (url.startsWith('http')) {
const urlObj = new URL(url);
urlObj.searchParams.set('force_web_search', 'true');
url = urlObj.toString();
} else {
// For relative URLs, append the parameter manually
const separator = url.includes('?') ? '&' : '?';
url = `${url}${separator}force_web_search=true`;
}
}
return fetchAPI(url, init);
@@ -0,0 +1,112 @@
import { Box, BoxButton, Icon, Text } from '@/components';
// Define Attachment type locally (mirroring backend structure)
export interface Attachment {
name?: string;
contentType?: string;
url: string;
}
interface AttachmentListProps {
attachments: Attachment[];
onRemove?: (index: number) => void;
isReadOnly?: boolean;
}
export const AttachmentList = ({
attachments,
onRemove,
isReadOnly = false,
}: AttachmentListProps) => {
if (!attachments || attachments.length === 0) {
return null;
}
return (
<Box
$direction="row"
$gap="0.5rem"
$width="100%"
$css={`
overflow-x: scroll;
`}
>
{attachments.map((attachment, idx) => {
const { name } = attachment;
const removeAttachment = () => {
if (onRemove) {
onRemove(idx);
}
};
return (
<Box
key={(name || 'attachment') + idx}
$direction="column"
$align="center"
>
<Box
$background="var(--c--theme--colors--greyscale-050)"
$minWidth="200px"
$direction="row"
$align="center"
$padding="xs"
$css={`
border-radius: 4px;
`}
>
{/* Extension du fichier */}
<Box
$background="var(--c--theme--colors--greyscale-200)"
$width="24px"
$height="24px"
$direction="row"
$align="center"
$margin={{ right: 'xs' }}
$justify="center"
$css={`
flex-shrink: 0;
border-radius: 4px;
`}
>
<Text
$color="var(--c--theme--colors--greyscale-700)"
$weight="500"
$css={`
font-size: 8px;
`}
>
{name?.split('.').pop()?.toUpperCase() || 'FILE'}
</Text>
</Box>
<Text
$size="sm"
$color="var(--c--theme--colors--greyscale-850)"
$css={`
overflow: hidden;
text-overflow: ellipsis;
white-space: initial;
display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
flex: 1;
`}
>
{name}
</Text>
{!isReadOnly && onRemove && (
<BoxButton
aria-label="Remove attachment"
onClick={removeAttachment}
>
<Icon iconName="close" $theme="greyscale" $size="18px" />
</BoxButton>
)}
</Box>
</Box>
);
})}
</Box>
);
};
@@ -19,6 +19,7 @@ import { Box, Icon, Loader, Text } from '@/components';
import { useChat } from '@/features/chat/api/useChat';
import { getConversation } from '@/features/chat/api/useConversation';
import { useCreateChatConversation } from '@/features/chat/api/useCreateConversation';
import { AttachmentList } from '@/features/chat/components/AttachmentList';
import { InputChat } from '@/features/chat/components/InputChat';
import { SourceItemList } from '@/features/chat/components/SourceItemList';
import { ToolInvocationItem } from '@/features/chat/components/ToolInvocationItem';
@@ -42,7 +43,7 @@ export const Chat = ({
}) => {
const { t } = useTranslation();
const copyToClipboard = useClipboard();
const { isDesktop } = useResponsiveStore();
const { isMobile } = useResponsiveStore();
const streamProtocol = 'data'; // or 'text'
const [forceWebSearch, setForceWebSearch] = useState(false);
@@ -374,14 +375,7 @@ export const Chat = ({
flex-basis: auto;
height: 100%;
flex-grow: 1;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
animation: fade-in 0.3s ease-out forwards;
`}
>
<Box
@@ -396,8 +390,8 @@ export const Chat = ({
flex-grow: 1;
position: relative;
margin-bottom: 0;
height: ${messages.length > 1 ? 'calc(100vh - 62px)' : '0'};
max-height: ${messages.length > 1 ? 'calc(100vh - 62px)' : '0'};
height: ${messages.length > 0 ? 'calc(100vh - 62px)' : '0'};
max-height: ${messages.length > 0 ? 'calc(100vh - 62px)' : '0'};
`}
>
{messages.length > 0 && (
@@ -407,7 +401,7 @@ export const Chat = ({
key={message.id}
data-message-id={message.id}
$css={`
display: flex;
display: flex;
width: 100%;
margin: auto;
padding-left: 12px;
@@ -419,23 +413,45 @@ export const Chat = ({
<Box
$gap="2"
$radius="8px"
$maxWidth="100%"
$padding={`${message.role === 'user' ? '12px' : '0'}`}
$margin={{ vertical: 'base' }}
$background={`${message.role === 'user' ? '#EEF1F4' : 'white'}`}
>
{/* Message content */}
{message.content && (
<Markdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Custom components for Markdown rendering
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node, ...props }) => <Text {...props} />,
}}
<Box
$css={`
opacity: 0;
animation: fade-in 0.3s ease-in-out forwards;
`}
>
{message.content}
</Markdown>
<Markdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Custom components for Markdown rendering
// eslint-disable-next-line @typescript-eslint/no-unused-vars
p: ({ node, ...props }) => <Text {...props} />,
}}
>
{message.content}
</Markdown>
</Box>
)}
{/* Attachments section */}
{message.experimental_attachments &&
message.experimental_attachments.length > 0 && (
<Box>
<AttachmentList
attachments={message.experimental_attachments}
isReadOnly={true}
/>
</Box>
)}
{/* Reasoning and tool invocations */}
<Box $direction="column" $gap="2">
{message.parts
?.filter(
@@ -461,25 +477,6 @@ export const Chat = ({
/>
) : null,
)}
{/* Show attachments if present */}
{message.experimental_attachments?.map(
(attachment: Attachment, index: number) =>
attachment.contentType?.includes('text/') ||
attachment.contentType?.includes('image/') ? (
<div
key={`${message.id}-${index}`}
style={{
display: 'block',
width: 'auto',
fontSize: 12,
borderRadius: 8,
color: '#888',
}}
>
{attachment.name}
</div>
) : null,
)}
</Box>
{message.role !== 'user' && (
<Box
@@ -522,7 +519,7 @@ export const Chat = ({
$variation="600"
$size="16px"
/>
{isDesktop && (
{!isMobile && (
<Text $color="#626A80" $weight="500">
{t('Copy')}
</Text>
@@ -564,7 +561,7 @@ export const Chat = ({
$variation="600"
$size="16px"
/>
{isDesktop && (
{!isMobile && (
<Text $color="#626A80" $weight="500">
{t('Show sources')}
</Text>
@@ -607,6 +604,7 @@ export const Chat = ({
position: relative;
bottom: 20px;
margin: auto;
z-index: 1000;
`}
$gap="6px"
$height="auto"
@@ -2,9 +2,10 @@ import { Button } from '@openfun/cunningham-react';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, Icon, Text } from '@/components';
import { Box, Icon, Text } from '@/components';
import { useResponsiveStore } from '@/stores';
import { AttachmentList } from './AttachmentList';
import { ScrollDown } from './ScrollDown';
import { SendButton } from './SendButton';
@@ -40,7 +41,7 @@ export const InputChat = ({
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragActive, setIsDragActive] = useState(false);
const { isDesktop } = useResponsiveStore();
const { isDesktop, isMobile } = useResponsiveStore();
const [currentSuggestionIndex, setCurrentSuggestionIndex] = useState(0);
const suggestions = [
@@ -69,55 +70,28 @@ export const InputChat = ({
width: 100%;
padding: ${isDesktop ? '0' : '0 10px'};
max-width: 750px;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideOutToBottom {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(20px);
}
}
`}
>
{/* Bouton de scroll vers le bas */}
{messagesLength > 1 && containerRef && (
<Box
$css={`
{messagesLength > 1 &&
status !== 'streaming' &&
containerRef &&
onScrollToBottom && (
<Box
$css={`
position: relative;
bottom: 0;
height: 0;
width: 100%;
margin: auto;
max-width: 750px;
opacity: ${onScrollToBottom ? '1' : '0'};
transition: opacity 0.4s;
`}
>
<ScrollDown
onClick={onScrollToBottom || (() => {})}
containerRef={containerRef}
/>
</Box>
)}
`}
>
<ScrollDown
onClick={onScrollToBottom}
containerRef={containerRef}
/>
</Box>
)}
{/* Message de bienvenue */}
{messagesLength === 0 && (
<Box
@@ -127,7 +101,7 @@ export const InputChat = ({
$margin={{ horizontal: 'base', bottom: 'md', top: '0' }}
$css={`
opacity: 0;
animation: fadeIn 0.2s cubic-bezier(1,0,0,1) 0.2s both;
animation: fade-in 0.2s cubic-bezier(1,0,0,1) 0.2s both;
`}
>
<Text as="h2" $size="xl" $weight="600" $margin={{ all: '0' }}>
@@ -180,9 +154,9 @@ export const InputChat = ({
messagesLength === 0
? `
opacity: 0;
animation: fadeIn 0.4s cubic-bezier(1,0,0,1) 0s both;
animation: fade-in 0.4s cubic-bezier(1,0,0,1) 0s both;
`
: 'animation: fadeIn 0.4s cubic-bezier(1,0,0,1) 0.4s both;'
: 'animation: fade-in 0.4s cubic-bezier(1,0,0,1) 0.4s both;'
}
>
<Box
@@ -307,91 +281,26 @@ export const InputChat = ({
{/*Aperçu des fichiers*/}
{files && files.length > 0 && (
<Box
$direction="row"
$gap="0.5rem"
$width="100%"
$css={`
overflow-x: scroll;
`}
$margin={{ horizontal: '0', bottom: 'xs', top: 'xs' }}
$padding={{ horizontal: 'base' }}
>
{Array.from(files).map((file, idx) => {
const { type: _type, name } = file;
const removeFile = () => {
<AttachmentList
attachments={Array.from(files).map((file) => ({
name: file.name,
contentType: file.type,
url: URL.createObjectURL(file),
}))}
onRemove={(index) => {
const dt = new DataTransfer();
Array.from(files).forEach((f, i) => {
if (i !== idx) {
if (i !== index) {
dt.items.add(f);
}
});
setFiles(dt.files.length > 0 ? dt.files : null);
};
return (
<Box
key={name + idx}
$direction="column"
$align="center"
$gap="xs"
>
{/*{type.startsWith('image/') ? (
<Image
style={{ width: 96, borderRadius: 8 }}
src={URL.createObjectURL(file)}
alt={name}
width={96}
height={96}
/>
) : (
<Box
$background="var(--c--theme--colors--greyscale-100)"
$width="64px"
$height="80px"
$radius="md"
/>
)}*/}
<Box
$background="var(--c--theme--colors--greyscale-050)"
$width="200px"
$direction="row"
$align="center"
$padding="xs"
$css={`
border: 1px solid var(--c--theme--colors--greyscale-200);
border-radius: 8px;
`}
>
<Text
$size="sm"
$color="var(--c--theme--colors--greyscale-850)"
$css={`
overflow: hidden;
text-overflow: ellipsis;
white-space: initial;
display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
`}
>
{name}
</Text>
<BoxButton
aria-label="Remove file"
onClick={removeFile}
>
<Icon
iconName="close"
$theme="greyscale"
$size="18px"
/>
</BoxButton>
</Box>
</Box>
);
})}
}}
isReadOnly={false}
/>
</Box>
)}
<Box
@@ -416,19 +325,31 @@ export const InputChat = ({
/>
}
>
{isDesktop && <Text $weight="500">{t('Attach file')}</Text>}
{!isMobile && <Text $weight="500">{t('Attach file')}</Text>}
</Button>
{onToggleWebSearch && (
<Box
$css={
forceWebSearch
? `
$margin={{ left: '4px' }}
$css={`
${
isMobile
? `
.research-web-button {
padding-right: 8px !important;
}
`
: ''
}
${
forceWebSearch
? `
.c__button {
background-color: var(--c--theme--colors--primary-100) !important;
}
`
: ''
}
: ''
}
`}
>
<Button
size="small"
@@ -436,6 +357,7 @@ export const InputChat = ({
onClick={onToggleWebSearch}
aria-label={t('Research on the web')}
color="tertiary-text"
className="research-web-button"
icon={
<Icon
iconName="language"
@@ -445,14 +367,50 @@ export const InputChat = ({
/>
}
>
{isDesktop && (
{!isMobile && (
<Text
$color={forceWebSearch ? 'primary' : 'greyscale'}
$theme={forceWebSearch ? 'primary' : 'greyscale'}
$weight="500"
>
{t('Research on the web')}
</Text>
)}
{isMobile && forceWebSearch && (
<Box
$direction="row"
$align="space-between"
$gap="xs"
$css={`
display: flex;
align-items: center;
line-height: 1;
`}
>
<Text
$theme="primary"
$weight="500"
$css={`
display: flex;
align-items: center;
`}
>
{t('Web')}
</Text>
<Icon
iconName="close"
$variation="800"
$theme="primary"
$size="md"
$css={`
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
padding-left: 4px;
`}
/>
</Box>
)}
</Button>
</Box>
)}
@@ -42,16 +42,7 @@ export const ScrollDown: React.FC<ScrollDownProps> = ({
z-index: 1000;
opacity: ${isVisible ? '1' : '0'};
transition: opacity 0.3s ease;
animation: fadeIn 0.3s ease;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
pointer-events: ${isVisible ? 'auto' : 'none'};
`}
>
<Button
@@ -24,18 +24,7 @@ export const SourceItemList: React.FC<SourceItemListProps> = ({ parts }) => {
margin-top: 0.5rem;
overflow: hidden;
opacity: 0;
animation: slideInFade 0.3s ease-out forwards;
@keyframes slideInFade {
from {
opacity: 0;
height: 0;
}
to {
opacity: 1;
height: auto;
}
}
animation: fade-in 0.3s ease-out forwards;
`}
>
{parts.map((part) => (
@@ -4,7 +4,6 @@ import { css } from 'styled-components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Header } from '@/features/header';
import { HEADER_HEIGHT } from '@/features/header/conf';
import { LeftPanel } from '@/features/left-panel';
import { useLeftPanelStore } from '@/features/left-panel/stores';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
@@ -22,6 +21,8 @@ export function MainLayout({
const { colorsTokens } = useCunninghamTheme();
const { togglePanel: _togglePanel, isPanelOpen } = useLeftPanelStore();
const HEADER_HEIGHT = `${isDesktop ? '65' : '52'}`;
return (
<Box className="--docs--main-layout">
<Box
@@ -98,3 +98,13 @@ ul a {
ul a:hover {
color: var(--c--theme--colors--primary-800);
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@@ -14,9 +14,9 @@ export const CONFIG = {
LANGUAGES: [
['en-us', 'English'],
['fr-fr', 'Français'],
['de-de', 'Deutsch'],
['nl-nl', 'Nederlands'],
['es-es', 'Español'],
// ['de-de', 'Deutsch'],
// ['nl-nl', 'Nederlands'],
// ['es-es', 'Español'],
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
@@ -80,10 +80,10 @@ export const TestLanguage = {
label: 'Français',
expectedLocale: ['fr-fr'],
},
German: {
label: 'Deutsch',
expectedLocale: ['de-de'],
},
// German: {
// label: 'Deutsch',
// expectedLocale: ['de-de'],
// },
} as const;
type TestLanguageKey = keyof typeof TestLanguage;