diff --git a/src/frontend/apps/conversations/package.json b/src/frontend/apps/conversations/package.json index 1f11372..affd06a 100644 --- a/src/frontend/apps/conversations/package.json +++ b/src/frontend/apps/conversations/package.json @@ -78,6 +78,7 @@ "jest-environment-jsdom": "29.7.0", "node-fetch": "2.7.0", "prettier": "3.5.3", + "sass": "^1.97.3", "stylelint": "16.20.0", "stylelint-config-standard": "38.0.0", "stylelint-prettier": "5.0.3", diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/book-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/book-filled.svg new file mode 100644 index 0000000..d1bb202 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/book-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/bookmark-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/bookmark-filled.svg new file mode 100644 index 0000000..fe03cc0 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/bookmark-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/car-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/car-filled.svg new file mode 100644 index 0000000..a19cada --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/car-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/chart-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/chart-filled.svg new file mode 100644 index 0000000..6341070 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/chart-filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/checkmark-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/checkmark-filled.svg new file mode 100644 index 0000000..683ddab --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/checkmark-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/euro-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/euro-filled.svg new file mode 100644 index 0000000..ff85424 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/euro-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/file-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/file-filled.svg new file mode 100644 index 0000000..3b511b4 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/file-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/folder-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/folder-filled.svg new file mode 100644 index 0000000..4727ee1 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/folder-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/folder-plus.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/folder-plus.svg new file mode 100644 index 0000000..a70d3ba --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/folder-plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/gear-rounded-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/gear-rounded-filled.svg new file mode 100644 index 0000000..70b5d71 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/gear-rounded-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/justice-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/justice-filled.svg new file mode 100644 index 0000000..6bf57e1 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/justice-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/key-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/key-filled.svg new file mode 100644 index 0000000..cf72d3d --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/key-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/lasuite-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/lasuite-filled.svg new file mode 100644 index 0000000..6b83204 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/lasuite-filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/megaphone-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/megaphone-filled.svg new file mode 100644 index 0000000..b8d9d61 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/megaphone-filled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/music-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/music-filled.svg new file mode 100644 index 0000000..81dc34c --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/music-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/palette-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/palette-filled.svg new file mode 100644 index 0000000..23ea132 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/palette-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/perso-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/perso-filled.svg new file mode 100644 index 0000000..8fbfec8 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/perso-filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/picture-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/picture-filled.svg new file mode 100644 index 0000000..7f8f6a2 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/picture-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/picture.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/picture.svg new file mode 100644 index 0000000..59e567b --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/picture.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/puzzle-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/puzzle-filled.svg new file mode 100644 index 0000000..8569cec --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/puzzle-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/assets/icons/uikit-custom/star-filled.svg b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/star-filled.svg new file mode 100644 index 0000000..9e12cc0 --- /dev/null +++ b/src/frontend/apps/conversations/src/assets/icons/uikit-custom/star-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/frontend/apps/conversations/src/cunningham/useCunninghamTheme.tsx b/src/frontend/apps/conversations/src/cunningham/useCunninghamTheme.tsx index f3e2fc2..b28876b 100644 --- a/src/frontend/apps/conversations/src/cunningham/useCunninghamTheme.tsx +++ b/src/frontend/apps/conversations/src/cunningham/useCunninghamTheme.tsx @@ -1,9 +1,7 @@ import merge from 'lodash/merge'; import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore'; -import { safeLocalStorage } from '@/utils/storages'; import { tokens } from './cunningham-tokens'; @@ -31,7 +29,6 @@ interface ThemeStore { theme: Theme; baseTheme: Theme; // 'default' or 'dsfr' (not persisted) themeTokens: Partial; - isDarkMode: boolean; toggleDarkMode: () => void; } @@ -53,15 +50,6 @@ const getComponentTokens = ( const DEFAULT_THEME: Theme = 'default'; const defaultTokens = getMergedTokens(DEFAULT_THEME); -// Helper to get isDarkMode from useChatPreferencesStore -const getIsDarkModeFromPreferences = (): boolean => { - try { - return useChatPreferencesStore.getState().isDarkModePreference ?? false; - } catch { - return false; - } -}; - const initialState: ThemeStore = { colorsTokens: defaultTokens.globals.colors, componentTokens: getComponentTokens(defaultTokens), @@ -73,91 +61,46 @@ const initialState: ThemeStore = { theme: DEFAULT_THEME, baseTheme: DEFAULT_THEME, themeTokens: defaultTokens.globals, - isDarkMode: getIsDarkModeFromPreferences(), toggleDarkMode: () => {}, }; -export const useCunninghamTheme = create()( - persist( - (set) => ({ - ...initialState, - setTheme: (theme: Theme) => { - // Extract base theme (default or dsfr) - const baseTheme: Theme = - theme === 'dark' || theme === 'dsfr-dark' - ? theme === 'dark' - ? 'default' - : 'dsfr' - : theme; +const getIsDarkMode = () => + useChatPreferencesStore.getState().isDarkModePreference; - const isDarkMode = - getIsDarkModeFromPreferences() ?? - (theme === 'dark' || theme === 'dsfr-dark'); +const resolveTheme = (baseTheme: Theme, isDarkMode: boolean): Theme => + isDarkMode ? (baseTheme === 'dsfr' ? 'dsfr-dark' : 'dark') : baseTheme; - // Apply dark mode based on stored preference or theme - const finalTheme: Theme = isDarkMode - ? baseTheme === 'dsfr' - ? 'dsfr-dark' - : 'dark' - : baseTheme; +const computeThemeState = (baseTheme: Theme, isDarkMode: boolean) => { + const theme = resolveTheme(baseTheme, isDarkMode); + const mergedTokens = getMergedTokens(theme); + return { + colorsTokens: mergedTokens.globals.colors, + componentTokens: getComponentTokens(mergedTokens), + contextualTokens: mergedTokens.contextuals, + currentTokens: tokens.themes[theme] as Partial, + fontSizesTokens: mergedTokens.globals.font.sizes, + spacingsTokens: mergedTokens.globals.spacings, + theme, + baseTheme, + themeTokens: mergedTokens.globals, + }; +}; - const newTokens = getMergedTokens(finalTheme); +export const useCunninghamTheme = create()((set) => ({ + ...initialState, + setTheme: (theme: Theme) => { + const baseTheme: Theme = + theme === 'dark' || theme === 'dsfr-dark' + ? theme === 'dark' + ? 'default' + : 'dsfr' + : theme; - set({ - colorsTokens: newTokens.globals.colors, - componentTokens: getComponentTokens(newTokens), - contextualTokens: newTokens.contextuals, - currentTokens: tokens.themes[finalTheme] as Partial, - fontSizesTokens: newTokens.globals.font.sizes, - spacingsTokens: newTokens.globals.spacings, - theme: finalTheme, - baseTheme, - themeTokens: newTokens.globals, - isDarkMode, - }); - }, - toggleDarkMode: () => { - useChatPreferencesStore.getState().toggleDarkModePreferences(); + set(computeThemeState(baseTheme, getIsDarkMode())); + }, + toggleDarkMode: () => { + useChatPreferencesStore.getState().toggleDarkModePreferences(); - set((state) => { - const newIsDarkMode = getIsDarkModeFromPreferences(); - const newTheme: Theme = newIsDarkMode - ? state.baseTheme === 'dsfr' - ? 'dsfr-dark' - : 'dark' - : state.baseTheme; - - const newTokens = getMergedTokens(newTheme); - - return { - colorsTokens: newTokens.globals.colors, - componentTokens: getComponentTokens(newTokens), - contextualTokens: newTokens.contextuals, - currentTokens: tokens.themes[newTheme] as Partial, - fontSizesTokens: newTokens.globals.font.sizes, - spacingsTokens: newTokens.globals.spacings, - theme: newTheme, - baseTheme: state.baseTheme, - themeTokens: newTokens.globals, - isDarkMode: newIsDarkMode, - }; - }); - }, - }), - { - name: 'cunningham-theme', - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment - storage: safeLocalStorage as any, - partialize: (state) => ({ isDarkMode: state.isDarkMode }), - onRehydrateStorage: () => (state, error) => { - if (error) { - console.error('[useCunninghamTheme] Rehydration error:', error); - return; - } - if (state) { - state.isDarkMode = getIsDarkModeFromPreferences(); - } - }, - }, - ), -); + set((state) => computeThemeState(state.baseTheme, getIsDarkMode())); + }, +})); diff --git a/src/frontend/apps/conversations/src/features/chat/api/useConversations.tsx b/src/frontend/apps/conversations/src/features/chat/api/useConversations.tsx index 8ede8d8..415a9f2 100644 --- a/src/frontend/apps/conversations/src/features/chat/api/useConversations.tsx +++ b/src/frontend/apps/conversations/src/features/chat/api/useConversations.tsx @@ -43,6 +43,8 @@ export const getConversations = async ( searchParams.set('title', params.title); } + searchParams.set("project", "none") + const response = await fetchAPI(`chats/?${searchParams.toString()}`); if (!response.ok) { diff --git a/src/frontend/apps/conversations/src/features/chat/api/useCreateProject.tsx b/src/frontend/apps/conversations/src/features/chat/api/useCreateProject.tsx new file mode 100644 index 0000000..fa41f0d --- /dev/null +++ b/src/frontend/apps/conversations/src/features/chat/api/useCreateProject.tsx @@ -0,0 +1,62 @@ +import { + UseMutationOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; +import { ChatProject } from '@/features/chat/types'; + +import { KEY_LIST_PROJECT } from './useProjects'; + +interface CreateProjectProps { + title: string; + icon: string; + color: string; + llm_instructions?: string; +} + +export const createProject = async ( + props: CreateProjectProps, +): Promise => { + const response = await fetchAPI('projects/', { + method: 'POST', + body: JSON.stringify(props), + }); + + if (!response.ok) { + throw new APIError( + 'Failed to create the project', + await errorCauses(response), + ); + } + + return response.json() as Promise; +}; + +type UseCreateProjectOptions = UseMutationOptions< + ChatProject, + APIError, + CreateProjectProps +>; + +export const useCreateProject = (options?: UseCreateProjectOptions) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createProject, + ...options, + onSuccess: (data, variables, context) => { + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_PROJECT], + }); + if (options?.onSuccess) { + void options.onSuccess(data, variables, context); + } + }, + onError: (error, variables, context) => { + if (options?.onError) { + void options.onError(error, variables, context); + } + }, + }); +}; diff --git a/src/frontend/apps/conversations/src/features/chat/api/useProjects.tsx b/src/frontend/apps/conversations/src/features/chat/api/useProjects.tsx new file mode 100644 index 0000000..38c1bba --- /dev/null +++ b/src/frontend/apps/conversations/src/features/chat/api/useProjects.tsx @@ -0,0 +1,79 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import { + APIError, + APIList, + errorCauses, + fetchAPI, + useAPIInfiniteQuery, +} from '@/api'; +import { ChatProject } from '@/features/chat/types'; + +const _projectsOrdering = [ + 'title', + '-title', + 'created_at', + '-created_at', + 'updated_at', + '-updated_at', +] as const; + +export type ProjectsOrdering = (typeof _projectsOrdering)[number]; + +export type ProjectsParams = { + page: number; + ordering?: ProjectsOrdering; + title?: string; + page_size?: number; +}; + +export type ProjectsResponse = APIList; + +export const getProjects = async ( + params: ProjectsParams, +): Promise => { + const searchParams = new URLSearchParams(); + if (params.page) { + searchParams.set('page', params.page.toString()); + } + + if (params.ordering) { + searchParams.set('ordering', params.ordering); + } + + if (params.title) { + searchParams.set('title', params.title); + } + + if (params.page_size) { + searchParams.set('page_size', params.page_size.toString()); + } + + const response = await fetchAPI(`projects/?${searchParams.toString()}`); + + if (!response.ok) { + throw new APIError( + 'Failed to get the projects', + await errorCauses(response), + ); + } + + return response.json() as Promise; +}; + +export const KEY_LIST_PROJECT = 'projects'; + +export function useProjects( + param: ProjectsParams, + queryConfig?: UseQueryOptions, +) { + return useQuery({ + queryKey: [KEY_LIST_PROJECT, param], + queryFn: () => getProjects(param), + ...queryConfig, + }); +} + +export const useInfiniteProjects = (params: ProjectsParams) => { + return useAPIInfiniteQuery(KEY_LIST_PROJECT, getProjects, params); +}; diff --git a/src/frontend/apps/conversations/src/features/chat/api/useRemoveConversation.tsx b/src/frontend/apps/conversations/src/features/chat/api/useRemoveConversation.tsx index 95fb016..147e762 100644 --- a/src/frontend/apps/conversations/src/features/chat/api/useRemoveConversation.tsx +++ b/src/frontend/apps/conversations/src/features/chat/api/useRemoveConversation.tsx @@ -7,6 +7,7 @@ import { import { APIError, errorCauses, fetchAPI } from '@/api'; import { KEY_LIST_CONVERSATION } from './useConversations'; +import { KEY_LIST_PROJECT } from './useProjects'; interface RemoveConversationProps { conversationId: string; @@ -44,6 +45,9 @@ export const useRemoveConversation = ( void queryClient.invalidateQueries({ queryKey: [KEY_LIST_CONVERSATION], }); + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_PROJECT], + }); if (options?.onSuccess) { void options.onSuccess(data, variables, context); } diff --git a/src/frontend/apps/conversations/src/features/chat/api/useRemoveProject.tsx b/src/frontend/apps/conversations/src/features/chat/api/useRemoveProject.tsx new file mode 100644 index 0000000..4f7242f --- /dev/null +++ b/src/frontend/apps/conversations/src/features/chat/api/useRemoveProject.tsx @@ -0,0 +1,55 @@ +import { + UseMutationOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; + +import { KEY_LIST_PROJECT } from './useProjects'; + +interface RemoveProjectProps { + projectId: string; +} + +export const removeProject = async ({ + projectId, +}: RemoveProjectProps): Promise => { + const response = await fetchAPI(`projects/${projectId}/`, { + method: 'DELETE', + }); + + if (!response.ok) { + throw new APIError( + 'Failed to delete the project', + await errorCauses(response), + ); + } +}; + +type UseRemoveProjectOptions = UseMutationOptions< + void, + APIError, + RemoveProjectProps +>; + +export const useRemoveProject = (options?: UseRemoveProjectOptions) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: removeProject, + ...options, + onSuccess: (data, variables, context) => { + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_PROJECT], + }); + if (options?.onSuccess) { + void options.onSuccess(data, variables, context); + } + }, + onError: (error, variables, context) => { + if (options?.onError) { + void options.onError(error, variables, context); + } + }, + }); +}; diff --git a/src/frontend/apps/conversations/src/features/chat/api/useRenameConversation.tsx b/src/frontend/apps/conversations/src/features/chat/api/useRenameConversation.tsx index 201aafb..763787d 100644 --- a/src/frontend/apps/conversations/src/features/chat/api/useRenameConversation.tsx +++ b/src/frontend/apps/conversations/src/features/chat/api/useRenameConversation.tsx @@ -7,6 +7,7 @@ import { import { APIError, errorCauses, fetchAPI } from '@/api'; import { KEY_LIST_CONVERSATION } from './useConversations'; +import { KEY_LIST_PROJECT } from './useProjects'; interface RenameConversationProps { conversationId: string; @@ -49,6 +50,9 @@ export const useRenameConversation = ( void queryClient.invalidateQueries({ queryKey: [KEY_LIST_CONVERSATION], }); + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_PROJECT], + }); if (options?.onSuccess) { void options.onSuccess(data, variables, context); } diff --git a/src/frontend/apps/conversations/src/features/chat/api/useUpdateProject.tsx b/src/frontend/apps/conversations/src/features/chat/api/useUpdateProject.tsx new file mode 100644 index 0000000..54f508e --- /dev/null +++ b/src/frontend/apps/conversations/src/features/chat/api/useUpdateProject.tsx @@ -0,0 +1,61 @@ +import { + UseMutationOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; + +import { KEY_LIST_PROJECT } from './useProjects'; + +interface UpdateProjectProps { + projectId: string; + title?: string; + icon?: string; + color?: string; + llm_instructions?: string; +} + +export const updateProject = async ({ + projectId, + ...fields +}: UpdateProjectProps): Promise => { + const response = await fetchAPI(`projects/${projectId}/`, { + method: 'PATCH', + body: JSON.stringify(fields), + }); + + if (!response.ok) { + throw new APIError( + 'Failed to update the project', + await errorCauses(response), + ); + } +}; + +type UseUpdateProjectOptions = UseMutationOptions< + void, + APIError, + UpdateProjectProps +>; + +export const useUpdateProject = (options?: UseUpdateProjectOptions) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: updateProject, + ...options, + onSuccess: (data, variables, context) => { + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_PROJECT], + }); + if (options?.onSuccess) { + void options.onSuccess(data, variables, context); + } + }, + onError: (error, variables, context) => { + if (options?.onError) { + void options.onError(error, variables, context); + } + }, + }); +}; diff --git a/src/frontend/apps/conversations/src/features/chat/types.tsx b/src/frontend/apps/conversations/src/features/chat/types.tsx index 59439d6..587f474 100644 --- a/src/frontend/apps/conversations/src/features/chat/types.tsx +++ b/src/frontend/apps/conversations/src/features/chat/types.tsx @@ -9,3 +9,18 @@ export interface ChatConversation { updated_at: string; title?: string; } +export interface ChatProjectConversation { + id: string; + title?: string; +} + +export interface ChatProject { + id: string; + title: string; + created_at: string; + updated_at: string; + icon: string; + color: string; + llm_instructions: string; + conversations: ChatProjectConversation[]; +} diff --git a/src/frontend/apps/conversations/src/features/home/components/HomeBanner.tsx b/src/frontend/apps/conversations/src/features/home/components/HomeBanner.tsx index 6b1320e..044c576 100644 --- a/src/frontend/apps/conversations/src/features/home/components/HomeBanner.tsx +++ b/src/frontend/apps/conversations/src/features/home/components/HomeBanner.tsx @@ -3,13 +3,12 @@ import _Image from 'next/image'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; -// import IconAssistant from '@/assets/logo/assistant.svg'; import IconAssistant from '@/assets/logo/logo-beta.svg'; import { Box, Icon, Text } from '@/components'; import { productName } from '@/core'; import { useCunninghamTheme } from '@/cunningham'; +import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore'; import { gotoLogin } from '@/features/auth'; -// import { ProConnectButton } from '@/features/auth'; import { useResponsiveStore } from '@/stores'; import BannerDarkMode from '../assets/banner-dark.svg'; @@ -19,8 +18,11 @@ import { getHeaderHeight } from './HomeHeader'; export default function HomeBanner() { const { t } = useTranslation(); - const { spacingsTokens, colorsTokens, componentTokens, isDarkMode } = + const { spacingsTokens, colorsTokens, componentTokens } = useCunninghamTheme(); + const isDarkMode = useChatPreferencesStore( + (state) => state.isDarkModePreference, + ); const { isMobile, isSmallMobile } = useResponsiveStore(); const withProConnect = Boolean( (componentTokens as Record)['home-proconnect'], diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ConversationItemActions.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ConversationItemActions.tsx index d93fd00..6a43f43 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/ConversationItemActions.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ConversationItemActions.tsx @@ -3,14 +3,13 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { DropdownMenu, DropdownMenuOption, Icon } from '@/components'; -import { ChatConversation } from '@/features/chat/types'; import { useOwnModal } from '@/features/left-panel/hooks/useModalHook'; import { ModalRemoveConversation } from './ModalRemoveConversation'; import { ModalRenameConversation } from './ModalRenameConversation'; interface ConversationItemActionsProps { - conversation: ChatConversation; + conversation: { id: string; title?: string }; } export const ConversationItemActions = ({ diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ConversationRow.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ConversationRow.tsx new file mode 100644 index 0000000..8c30038 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ConversationRow.tsx @@ -0,0 +1,92 @@ +import { memo, useCallback, useMemo, type ReactNode } from 'react'; +import { css } from 'styled-components'; + +import { Box, StyledLink } from '@/components'; +import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore'; +import { useResponsiveStore } from '@/stores'; + +type ConversationRowProps = { + conversationId: string; + isActive: boolean; + children: ReactNode; + actions?: ReactNode; +}; + +const linkStyles = css` + overflow: auto; + flex-grow: 1; + color: var(--c--theme--colors--greyscale-900); +`; + +const getBoxStyles = (isActive: boolean, isDesktop: boolean) => css` + border-radius: 4px; + width: 100%; + margin-bottom: 1px; + background-color: ${isActive + ? 'var(--c--contextuals--background--semantic--overlay--primary)' + : ''}; + font-weight: ${isActive ? '700' : '500'}; + transition: background-color 0.2s cubic-bezier(1, 0, 0, 1); + .pinned-actions { + padding: 2px 0; + opacity: ${isDesktop ? 0 : 1}; + background-color: transparent; + transition: all 0.3s cubic-bezier(1, 0, 0, 1); + } + &:hover, + &:focus, + &:focus-within { + background-color: var( + --c--contextuals--background--semantic--overlay--primary + ); + .pinned-actions { + opacity: 1; + } + } + .pinned-actions:focus-within { + opacity: 1; + } +`; + +const containerPadding = { horizontal: 'xs', vertical: '4px' }; + +export const ConversationRow = memo(function ConversationRow({ + conversationId, + isActive, + children, + actions, +}: ConversationRowProps) { + const isDesktop = useResponsiveStore((state) => state.isDesktop); + const setPanelOpen = useChatPreferencesStore((state) => state.setPanelOpen); + + const handleLinkClick = useCallback(() => { + if (!isDesktop) { + setPanelOpen(false); + } + }, [isDesktop, setPanelOpen]); + + const boxStyles = useMemo( + () => getBoxStyles(isActive, isDesktop), + [isActive, isDesktop], + ); + + return ( + + + {children} + + + {actions && {actions}} + + ); +}); diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelContent.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelContent.tsx index 7010bdc..f4c8c33 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelContent.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelContent.tsx @@ -1,6 +1,8 @@ import { Box } from '@/components'; import { useAuth } from '@/features/auth/hooks'; import { LeftPanelConversations } from '@/features/left-panel/components/LeftPanelConversations'; +import { LeftPanelProjects } from '@/features/left-panel/components/LeftPanelProjects'; + import { useResponsiveStore } from '@/stores'; export const LeftPanelContent = () => { @@ -17,7 +19,12 @@ export const LeftPanelContent = () => { height: calc(100dvh - ${isDesktop ? '52px' : '104px'}); `} > - {authenticated && } + {authenticated && ( + <> + {' '} + + + )} ); }; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx index 6069033..8d9c304 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelConversationItem.tsx @@ -1,12 +1,9 @@ -import { memo, useCallback, useMemo } from 'react'; -import { css } from 'styled-components'; +import { memo } from 'react'; -import { Box, StyledLink } from '@/components'; -import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore'; import { ChatConversation } from '@/features/chat/types'; import { ConversationItemActions } from '@/features/left-panel/components/ConversationItemActions'; +import { ConversationRow } from '@/features/left-panel/components/ConversationRow'; import { SimpleConversationItem } from '@/features/left-panel/components/SimpleConversationItem'; -import { useResponsiveStore } from '@/stores'; type LeftPanelConversationItemProps = { conversation: ChatConversation; @@ -15,92 +12,24 @@ type LeftPanelConversationItemProps = { showUpdatedAt?: boolean; }; -const linkStyles = css` - overflow: auto; - flex-grow: 1; - color: var(--c--theme--colors--greyscale-900); -`; - -const getBoxStyles = ( - isCurrentConversation: boolean, - isDesktop: boolean, -) => css` - border-radius: 4px; - width: 100%; - margin-bottom: 1px; - background-color: ${isCurrentConversation - ? 'var(--c--contextuals--background--semantic--overlay--primary)' - : ''}; - font-weight: ${isCurrentConversation ? '700' : '500'}; - transition: background-color 0.2s cubic-bezier(1, 0, 0, 1); - .pinned-actions { - padding: 2px 0; - opacity: ${isDesktop ? 0 : 1}; - background-color: transparent; - transition: all 0.3s cubic-bezier(1, 0, 0, 1); - } - &:hover, - &:focus, - &:focus-within { - background-color: var( - --c--contextuals--background--semantic--overlay--primary - ); - .pinned-actions { - opacity: 1; - } - } - .pinned-actions:focus-within { - opacity: 1; - } -`; - -const containerPadding = { horizontal: 'xs', vertical: '4px' }; - export const LeftPanelConversationItem = memo( function LeftPanelConversationItem({ conversation, isCurrentConversation, showUpdatedAt = false, }: LeftPanelConversationItemProps) { - const isDesktop = useResponsiveStore((state) => state.isDesktop); - const setPanelOpen = useChatPreferencesStore((state) => state.setPanelOpen); - - const handleLinkClick = useCallback(() => { - if (!isDesktop) { - setPanelOpen(false); - } - }, [isDesktop, setPanelOpen]); - - const boxStyles = useMemo( - () => getBoxStyles(isCurrentConversation, isDesktop), - [isCurrentConversation, isDesktop], - ); - return ( - } > - - - - - - - - + + ); }, ); diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeader.tsx index 36e5ced..c3909a1 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeader.tsx @@ -4,16 +4,19 @@ import { PropsWithChildren, useEffect, useState } from 'react'; import { Box } from '@/components'; import { useAuth } from '@/features/auth'; import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore'; +import { useOwnModal } from '@/features/left-panel/hooks/useModalHook'; import { useResponsiveStore } from '@/stores'; import { LeftPanelHeaderActions } from './LeftPanelHeaderActions'; import { LeftPanelSearchModal } from './LeftPanelSearchModal'; +import { ModalCreateProject } from './ModalCreateProject'; export const LeftPanelHeader = ({ children }: PropsWithChildren) => { const router = useRouter(); const { authenticated } = useAuth(); const { isDesktop } = useResponsiveStore(); const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); + const createProjectModal = useOwnModal(); const { setPanelOpen } = useChatPreferencesStore(); @@ -47,6 +50,7 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { } setIsSearchModalOpen(true); }} + onCreateProject={createProjectModal.open} /> {children} @@ -55,6 +59,9 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { {authenticated && isSearchModalOpen && ( setIsSearchModalOpen(false)} /> )} + {authenticated && createProjectModal.isOpen && ( + + )} ); }; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeaderActions.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeaderActions.tsx index 830a8f6..cdeb793 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeaderActions.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelHeaderActions.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; +import FolderPlusIcon from '@/assets/icons/uikit-custom/folder-plus.svg'; import NewChatIcon from '@/assets/icons/new-message-bold.svg'; import { Box, Icon } from '@/components'; @@ -20,11 +21,13 @@ const iconWrapperCss = css` type LeftPanelHeaderActionsProps = { onNewChat: () => void; onSearch: () => void; + onCreateProject: () => void; }; export const LeftPanelHeaderActions = ({ onNewChat, onSearch, + onCreateProject, }: LeftPanelHeaderActionsProps) => { const { t } = useTranslation(); @@ -56,6 +59,16 @@ export const LeftPanelHeaderActions = ({ onClick={onSearch} aria-label={t('Search for a chat')} /> + + + + } + label={t('Create project')} + onClick={onCreateProject} + aria-label={t('Create project')} + /> ); }; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelProjectItem.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelProjectItem.tsx new file mode 100644 index 0000000..e0da015 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelProjectItem.tsx @@ -0,0 +1,158 @@ +import { memo, useCallback, useState } from 'react'; +import { css } from 'styled-components'; +import { useTranslation } from 'react-i18next'; + +import { Box, Icon, Text } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { ChatProject } from '@/features/chat/types'; +import { ConversationItemActions } from '@/features/left-panel/components/ConversationItemActions'; +import { ConversationRow } from '@/features/left-panel/components/ConversationRow'; +import { PROJECT_COLORS, PROJECT_ICONS } from '@/features/left-panel/components/project-constants'; +import { ProjectItemActions } from '@/features/left-panel/components/ProjectItemActions'; +type LeftPanelProjectItemProps = { + project: ChatProject; + currentConversationId?: string; +}; + +const headerStyles = css` + border-radius: 4px; + width: 100%; + cursor: pointer; + user-select: none; + transition: background-color 0.2s cubic-bezier(1, 0, 0, 1); + .pinned-actions { + opacity: 0; + transition: opacity 0.3s cubic-bezier(1, 0, 0, 1); + } + &:hover, + &:focus-within { + background-color: var( + --c--contextuals--background--semantic--overlay--primary + ); + .pinned-actions { + opacity: 1; + } + } +`; + +const conversationListStyles = css` + overflow: hidden; +`; + +const titleTextStyles = css` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const LeftPanelProjectItem = memo(function LeftPanelProjectItem({ + project, + currentConversationId, +}: LeftPanelProjectItemProps) { + const { t } = useTranslation(); + const { colorsTokens } = useCunninghamTheme(); + + const [isOpen, setIsOpen] = useState(false); + + const iconColor = + colorsTokens[PROJECT_COLORS[project.color] as keyof typeof colorsTokens] ?? + undefined; + + const toggleOpen = useCallback(() => { + setIsOpen((prev) => !prev); + }, []); + + return ( + + {/* Project header - click to toggle */} + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleOpen(); + } + }} + > + + + + {(() => { + const IconComp = PROJECT_ICONS[project.icon] ?? PROJECT_ICONS.folder; + return ( + + + + ); + })()} + + {project.title} + + + + e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + + + + + {/* Conversations list */} + {isOpen && ( + + {project.conversations.length === 0 ? ( + + {t('No conversations')} + + ) : ( + project.conversations.map((conv) => ( + } + > + + {conv.title || t('Untitled conversation')} + + + )) + )} + + )} + + ); +}); diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelProjects.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelProjects.tsx new file mode 100644 index 0000000..3f5e9c0 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/LeftPanelProjects.tsx @@ -0,0 +1,57 @@ +import { useRouter } from 'next/router'; +import { useTranslation } from 'react-i18next'; + +import { Box, InfiniteScroll, Text } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { useInfiniteProjects } from '@/features/chat/api/useProjects'; +import { LeftPanelProjectItem } from '@/features/left-panel/components/LeftPanelProjectItem'; + +export const LeftPanelProjects = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { id } = router.query; + + const { spacingsTokens } = useCunninghamTheme(); + + const projects = useInfiniteProjects({ + page: 1, + }); + + const mainProjects = + projects.data?.pages.flatMap((page) => page.results) || []; + + if (mainProjects.length === 0) { + return null; + } + + return ( + + + + {t('Your projects')} + + +
+ {mainProjects.map((prj) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ModalCreateProject.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ModalCreateProject.tsx new file mode 100644 index 0000000..3984626 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ModalCreateProject.tsx @@ -0,0 +1,226 @@ +import { Button, Input, Modal, ModalSize } from '@openfun/cunningham-react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { Box, Text, useToast } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { useCreateProject } from '@/features/chat/api/useCreateProject'; +import { useOwnModal } from '@/features/left-panel/hooks/useModalHook'; + +import { PROJECT_COLORS, PROJECT_ICONS } from './project-constants'; +import { ModalIconColorPicker } from './ModalIconColorPicker'; + +const textareaCss = css` + width: 100%; + min-height: 120px; + padding: var(--c--globals--spacings--xs); + border: 1px solid var(--c--contextuals--border--semantic--neutral--default); + border-radius: 4px; + font-family: var(--c--globals--font--families--base); + font-size: 0.875rem; + resize: vertical; + + &:focus { + outline: none; + border-color: var(--c--contextuals--border--semantic--info--default); + } +`; + +const avatarButtonCss = css` + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--c--contextuals--border--semantic--neutral--default); + border-radius: 8px; + background-color: transparent; + cursor: pointer; + flex-shrink: 0; + transition: border-color 0.15s ease; + + &:hover { + border-color: var(--c--contextuals--border--semantic--info--default); + } +`; + +interface ModalCreateProjectProps { + onClose: () => void; +} + +export const ModalCreateProject = ({ onClose }: ModalCreateProjectProps) => { + const { showToast } = useToast(); + const { t } = useTranslation(); + + const { colorsTokens } = useCunninghamTheme(); + const iconColorModal = useOwnModal(); + + const [name, setName] = useState(''); + const [icon, setIcon] = useState('folder'); + const [color, setColor] = useState('color_1'); + const [instructions, setInstructions] = useState(''); + + const IconComp = PROJECT_ICONS[icon] ?? PROJECT_ICONS.folder; + const iconColor = + colorsTokens[PROJECT_COLORS[color] as keyof typeof colorsTokens] ?? + undefined; + + const { mutate: createProject, isPending } = useCreateProject({ + onSuccess: () => { + showToast( + 'success', + t('The project has been created.'), + undefined, + 4000, + ); + onClose(); + }, + onError: (error) => { + const errorMessage = + error.cause?.[0] || + error.message || + t('An error occurred while creating the project'); + showToast('error', errorMessage, undefined, 4000); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmedName = name.trim(); + if (!trimmedName) return; + + createProject({ + title: trimmedName, + icon, + color, + llm_instructions: instructions.trim() || undefined, + }); + }; + + return ( + <> + + + + + } + size={ModalSize.MEDIUM} + title={ + + {t('New project')} + + } + > + +
+ + + + {t('Project avatar and name')} + + + + + + + + + ) => + setName(e.target.value) + } + required + /> + + + + + + + {t('Instructions (experimental)')} + + ) => + setInstructions(e.target.value) + } + placeholder={t( + 'Example: Be concise and maintain a professional tone.', + )} + /> + + {t( + 'Use this field to provide any additional context the Assistant may need. This feature is being tested. It may not work as expected.', + )} + + + + +
+
+ {iconColorModal.isOpen && ( + + )} + + ); +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ModalIconColorPicker.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ModalIconColorPicker.tsx new file mode 100644 index 0000000..3a40422 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ModalIconColorPicker.tsx @@ -0,0 +1,155 @@ +import { Modal, ModalSize } from '@openfun/cunningham-react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; + +import { PROJECT_COLORS, PROJECT_ICONS } from './project-constants'; + +const iconEntries = Object.entries(PROJECT_ICONS); +const colorEntries = Object.entries(PROJECT_COLORS); + +const gridCss = css` + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 6px; + justify-items: center; +`; + +const iconCellCss = css` + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + cursor: pointer; + border: none; + background-color: transparent; + color: var(--c--contextuals--content--semantic--neutral--secondary); + transition: background-color 0.15s ease; + + & svg { + fill: currentColor; + } + + &:hover { + background-color: var( + --c--contextuals--background--semantic--overlay--primary + ); + } +`; + +const iconSelectedCss = css` + ${iconCellCss} + background-color: var( + --c--contextuals--background--semantic--overlay--primary + ); +`; + +const colorCellCss = css` + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 8px; + cursor: pointer; + border: none; + background-color: transparent; + transition: background-color 0.15s ease; + + &:hover { + background-color: var( + --c--contextuals--background--semantic--overlay--primary + ); + } +`; + +const colorSelectedCss = css` + ${colorCellCss} + background-color: var( + --c--contextuals--background--semantic--overlay--primary + ); +`; + +const separatorCss = css` + height: 1px; + background-color: var(--c--contextuals--border--semantic--neutral--default); +`; + +interface ModalIconColorPickerProps { + icon: string; + color: string; + onIconChange: (icon: string) => void; + onColorChange: (color: string) => void; + onClose: () => void; +} + +export const ModalIconColorPicker = ({ + icon, + color, + onIconChange, + onColorChange, + onClose, +}: ModalIconColorPickerProps) => { + const { t } = useTranslation(); + const { colorsTokens } = useCunninghamTheme(); + + return ( + + + + {iconEntries.map(([key, IconComp]) => ( + onIconChange(key)} + aria-label={key} + aria-pressed={icon === key} + > + + + ))} + + + + + + {colorEntries.map(([key, token]) => ( + onColorChange(key)} + aria-label={key} + aria-pressed={color === key} + > + + + ))} + + + + ); +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ModalProjectSettings.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ModalProjectSettings.tsx new file mode 100644 index 0000000..a243c95 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ModalProjectSettings.tsx @@ -0,0 +1,234 @@ +import { Button, Input, Modal, ModalSize } from '@openfun/cunningham-react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { Box, Text, useToast } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { useUpdateProject } from '@/features/chat/api/useUpdateProject'; +import { ChatProject } from '@/features/chat/types'; +import { useOwnModal } from '@/features/left-panel/hooks/useModalHook'; + +import { PROJECT_COLORS, PROJECT_ICONS } from './project-constants'; +import { ModalIconColorPicker } from './ModalIconColorPicker'; + +const textareaCss = css` + width: 100%; + min-height: 120px; + padding: var(--c--globals--spacings--xs); + border: 1px solid var(--c--contextuals--border--semantic--neutral--default); + border-radius: 4px; + font-family: var(--c--globals--font--families--base); + font-size: 0.875rem; + resize: vertical; + + &:focus { + outline: none; + border-color: var(--c--contextuals--border--semantic--info--default); + } +`; + +const avatarButtonCss = css` + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--c--contextuals--border--semantic--neutral--default); + border-radius: 8px; + background-color: transparent; + cursor: pointer; + flex-shrink: 0; + transition: border-color 0.15s ease; + + &:hover { + border-color: var(--c--contextuals--border--semantic--info--default); + } +`; + +interface ModalProjectSettingsProps { + project: ChatProject; + onClose: () => void; +} + +export const ModalProjectSettings = ({ + project, + onClose, +}: ModalProjectSettingsProps) => { + const { showToast } = useToast(); + const { t } = useTranslation(); + + const { colorsTokens } = useCunninghamTheme(); + const iconColorModal = useOwnModal(); + + const [name, setName] = useState(project.title); + const [icon, setIcon] = useState(project.icon || 'folder'); + const [color, setColor] = useState(project.color || 'color_1'); + const [instructions, setInstructions] = useState( + project.llm_instructions || '', + ); + + const IconComp = PROJECT_ICONS[icon] ?? PROJECT_ICONS.folder; + const iconColor = + colorsTokens[PROJECT_COLORS[color] as keyof typeof colorsTokens] ?? + undefined; + + const { mutate: updateProject, isPending } = useUpdateProject({ + onSuccess: () => { + showToast( + 'success', + t('The project has been updated.'), + undefined, + 4000, + ); + onClose(); + }, + onError: (error) => { + const errorMessage = + error.cause?.[0] || + error.message || + t('An error occurred while updating the project'); + showToast('error', errorMessage, undefined, 4000); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmedName = name.trim(); + if (!trimmedName) return; + + updateProject({ + projectId: project.id, + title: trimmedName, + icon, + color, + llm_instructions: instructions.trim() || undefined, + }); + }; + + return ( + <> + + + + + } + size={ModalSize.MEDIUM} + title={ + + {t('Project settings')} + + } + > + +
+ + + + {t('Project avatar and name')} + + + + + + + + + ) => + setName(e.target.value) + } + required + /> + + + + + + + {t('Instructions (experimental)')} + + ) => + setInstructions(e.target.value) + } + placeholder={t( + 'Example: Be concise and maintain a professional tone.', + )} + /> + + {t( + 'Use this field to provide any additional context the Assistant may need. This feature is being tested. It may not work as expected.', + )} + + + + +
+
+ {iconColorModal.isOpen && ( + + )} + + ); +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveConversation.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveConversation.tsx index aa3e9ab..7127114 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveConversation.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveConversation.tsx @@ -5,11 +5,10 @@ import { useRouter } from 'next/router'; import { Box, Text, useToast } from '@/components'; import { useRemoveConversation } from '@/features/chat/api/useRemoveConversation'; -import { ChatConversation } from '@/features/chat/types'; interface ModalRemoveConversationProps { onClose: () => void; - conversation: ChatConversation; + conversation: { id: string; title?: string }; } export const ModalRemoveConversation = ({ diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveProject.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveProject.tsx new file mode 100644 index 0000000..9817a58 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ModalRemoveProject.tsx @@ -0,0 +1,96 @@ +import { Button, Modal, ModalSize } from '@openfun/cunningham-react'; +import { t } from 'i18next'; + +import { usePathname } from 'next/navigation'; +import { useRouter } from 'next/router'; + +import { Box, Text, useToast } from '@/components'; +import { useRemoveProject } from '@/features/chat/api/useRemoveProject'; + +interface ModalRemoveProjectProps { + onClose: () => void; + project: { id: string; title: string }; +} + +export const ModalRemoveProject = ({ + onClose, + project, +}: ModalRemoveProjectProps) => { + const { showToast } = useToast(); + const { push } = useRouter(); + const pathname = usePathname(); + + const { mutate: removeProject } = useRemoveProject({ + onSuccess: () => { + showToast( + 'success', + t('The project has been deleted.'), + undefined, + 4000, + ); + if (pathname === '/') { + onClose(); + } else { + void push('/'); + } + }, + }); + + return ( + onClose()} + aria-label={t('Content modal to delete project')} + rightActions={ + <> + + + + + } + size={ModalSize.MEDIUM} + title={ + + {t('Delete {{title}}', { title: project.title })} + + } + > + + + {t('Are you sure you want to delete the “{{title}}” project? All associated conversations and embedded' + + ' documents will be permanently lost.', { title: project.title })} + + + + ); +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ModalRenameConversation.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ModalRenameConversation.tsx index 62828fc..dc1c8e0 100644 --- a/src/frontend/apps/conversations/src/features/left-panel/components/ModalRenameConversation.tsx +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ModalRenameConversation.tsx @@ -4,11 +4,10 @@ import { useTranslation } from 'react-i18next'; import { Box, Text, useToast } from '@/components'; import { useRenameConversation } from '@/features/chat/api/useRenameConversation'; -import { ChatConversation } from '@/features/chat/types'; interface ModalRenameConversationProps { onClose: () => void; - conversation: ChatConversation; + conversation: { id: string; title?: string }; } export const ModalRenameConversation = ({ diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/ProjectItemActions.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/ProjectItemActions.tsx new file mode 100644 index 0000000..1db8994 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/ProjectItemActions.tsx @@ -0,0 +1,93 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { DropdownMenu, DropdownMenuOption, Icon } from '@/components'; +import { ChatProject } from '@/features/chat/types'; +import { useOwnModal } from '@/features/left-panel/hooks/useModalHook'; + +import { ModalProjectSettings } from './ModalProjectSettings'; +import { ModalRemoveProject } from './ModalRemoveProject'; + +interface ProjectItemActionsProps { + project: ChatProject; +} + +export const ProjectItemActions = ({ project }: ProjectItemActionsProps) => { + const { t } = useTranslation(); + + const deleteModal = useOwnModal(); + const settingsModal = useOwnModal(); + + const options: DropdownMenuOption[] = [ + { + label: t('Project settings'), + icon: 'settings', + callback: settingsModal.open, + disabled: false, + testId: `project-item-actions-settings-${project.id}`, + }, + { + label: t('Delete project'), + icon: 'delete', + callback: deleteModal.open, + disabled: false, + testId: `project-item-actions-remove-${project.id}`, + }, + ]; + const dropdownLabel = useMemo( + () => + t('Actions list for project {{title}}', { + title: project.title, + }), + [t, project.title], + ); + + return ( + <> + + + + + {deleteModal.isOpen && ( + + )} + {settingsModal.isOpen && ( + + )} + + ); +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/components/project-constants.tsx b/src/frontend/apps/conversations/src/features/left-panel/components/project-constants.tsx new file mode 100644 index 0000000..6e51879 --- /dev/null +++ b/src/frontend/apps/conversations/src/features/left-panel/components/project-constants.tsx @@ -0,0 +1,58 @@ +import { ComponentType, SVGProps } from 'react'; + +import BookIcon from '@/assets/icons/uikit-custom/book-filled.svg'; +import BookmarkIcon from '@/assets/icons/uikit-custom/bookmark-filled.svg'; +import CarIcon from '@/assets/icons/uikit-custom/car-filled.svg'; +import ChartIcon from '@/assets/icons/uikit-custom/chart-filled.svg'; +import CheckmarkIcon from '@/assets/icons/uikit-custom/checkmark-filled.svg'; +import EuroIcon from '@/assets/icons/uikit-custom/euro-filled.svg'; +import FileIcon from '@/assets/icons/uikit-custom/file-filled.svg'; +import FolderIcon from '@/assets/icons/uikit-custom/folder-filled.svg'; +import GearIcon from '@/assets/icons/uikit-custom/gear-rounded-filled.svg'; +import JusticeIcon from '@/assets/icons/uikit-custom/justice-filled.svg'; +import KeyIcon from '@/assets/icons/uikit-custom/key-filled.svg'; +import LaSuiteIcon from '@/assets/icons/uikit-custom/lasuite-filled.svg'; +import MegaphoneIcon from '@/assets/icons/uikit-custom/megaphone-filled.svg'; +import MusicIcon from '@/assets/icons/uikit-custom/music-filled.svg'; +import PaletteIcon from '@/assets/icons/uikit-custom/palette-filled.svg'; +import PersoIcon from '@/assets/icons/uikit-custom/perso-filled.svg'; +import PhotoIcon from '@/assets/icons/uikit-custom/picture-filled.svg'; +import PuzzleIcon from '@/assets/icons/uikit-custom/puzzle-filled.svg'; +import StarIcon from '@/assets/icons/uikit-custom/star-filled.svg'; + +export const PROJECT_COLORS: Record = { + color_1: 'warning-500', + color_2: 'warning-600', + color_3: 'orange-500', + color_4: 'brown-600', + color_5: 'green-650', + color_6: 'blue-1-600', + color_7: 'blue-2-600', + color_8: 'yellow-600', + color_9: 'purple-500', +}; + +export const PROJECT_ICONS: Record< + string, + ComponentType> +> = { + folder: FolderIcon, + file: FileIcon, + perso: PersoIcon, + gear: GearIcon, + megaphone: MegaphoneIcon, + star: StarIcon, + bookmark: BookmarkIcon, + chart: ChartIcon, + photo: PhotoIcon, + euro: EuroIcon, + key: KeyIcon, + justice: JusticeIcon, + book: BookIcon, + puzzle: PuzzleIcon, + palette: PaletteIcon, + car: CarIcon, + music: MusicIcon, + checkmark: CheckmarkIcon, + la_suite: LaSuiteIcon, +}; diff --git a/src/frontend/apps/conversations/src/features/left-panel/stores/index.ts b/src/frontend/apps/conversations/src/features/left-panel/stores/index.ts deleted file mode 100644 index 9db6dd9..0000000 --- a/src/frontend/apps/conversations/src/features/left-panel/stores/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useLeftPanelStore'; diff --git a/src/frontend/apps/conversations/src/features/left-panel/stores/useLeftPanelStore.tsx b/src/frontend/apps/conversations/src/features/left-panel/stores/useLeftPanelStore.tsx deleted file mode 100644 index 9c087d2..0000000 --- a/src/frontend/apps/conversations/src/features/left-panel/stores/useLeftPanelStore.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { create } from 'zustand'; - -interface LeftPanelState { - isPanelOpen: boolean; - togglePanel: (value?: boolean) => void; -} - -export const useLeftPanelStore = create((set, get) => ({ - isPanelOpen: false, - togglePanel: (value?: boolean) => { - const sanitizedValue = - value !== undefined && typeof value === 'boolean' - ? value - : !get().isPanelOpen; - - set({ isPanelOpen: sanitizedValue }); - }, -})); diff --git a/src/frontend/apps/conversations/src/features/settings/components/SettingsModal.tsx b/src/frontend/apps/conversations/src/features/settings/components/SettingsModal.tsx index 2489dc9..55c28e1 100644 --- a/src/frontend/apps/conversations/src/features/settings/components/SettingsModal.tsx +++ b/src/frontend/apps/conversations/src/features/settings/components/SettingsModal.tsx @@ -5,6 +5,7 @@ import { Box, Text, ToggleSwitch, useToast } from '@/components'; import { useUserUpdate } from '@/core/api/useUserUpdate'; import { useCunninghamTheme } from '@/cunningham'; import { useAuthQuery } from '@/features/auth/api'; +import { useChatPreferencesStore } from '@/features/chat/stores/useChatPreferencesStore'; interface SettingsModalProps { onClose: () => void; @@ -30,7 +31,10 @@ export const SettingsModal = ({ onClose, isOpen }: SettingsModalProps) => { const { data: user } = useAuthQuery(); const { mutateAsync: updateUser, isPending } = useUserUpdate(); const { showToast } = useToast(); - const { isDarkMode, toggleDarkMode } = useCunninghamTheme(); + const { toggleDarkMode } = useCunninghamTheme(); + const isDarkMode = useChatPreferencesStore( + (state) => state.isDarkModePreference, + ); const handleToggleChange = async ( field: 'allow_conversation_analytics' | 'allow_smart_web_search', diff --git a/src/frontend/apps/conversations/src/pages/_app.tsx b/src/frontend/apps/conversations/src/pages/_app.tsx index cd26d3e..393244c 100644 --- a/src/frontend/apps/conversations/src/pages/_app.tsx +++ b/src/frontend/apps/conversations/src/pages/_app.tsx @@ -6,7 +6,7 @@ import { AppProvider, productName } from '@/core/'; import { useCunninghamTheme } from '@/cunningham'; import '@/i18n/initI18n'; import { NextPageWithLayout } from '@/types/next'; - +import "@gouvfr-lasuite/ui-kit/style"; import './globals.css'; type AppPropsWithLayout = AppProps & { diff --git a/src/frontend/apps/conversations/src/tests/utils.tsx b/src/frontend/apps/conversations/src/tests/utils.tsx index b0d7c7d..90c2887 100644 --- a/src/frontend/apps/conversations/src/tests/utils.tsx +++ b/src/frontend/apps/conversations/src/tests/utils.tsx @@ -1,4 +1,5 @@ -import { CunninghamProvider } from '@openfun/cunningham-react'; +// import { CunninghamProvider } from '@openfun/cunningham-react'; +import { CunninghamProvider } from "@gouvfr-lasuite/ui-kit"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PropsWithChildren } from 'react'; diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 8a8bb29..d81d9d2 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -1362,10 +1362,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.39.2": - version "9.39.2" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" - integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== +"@eslint/js@9.39.3": + version "9.39.3" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.3.tgz#c6168736c7e0c43ead49654ed06a4bcb3833363d" + integrity sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw== "@eslint/object-schema@^2.1.7": version "2.1.7" @@ -2380,6 +2380,95 @@ dependencies: "@opentelemetry/core" "^1.1.0" +"@parcel/watcher-android-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564" + integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A== + +"@parcel/watcher-darwin-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz#88d3e720b59b1eceffce98dac46d7c40e8be5e8e" + integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA== + +"@parcel/watcher-darwin-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063" + integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg== + +"@parcel/watcher-freebsd-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53" + integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng== + +"@parcel/watcher-linux-arm-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a" + integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ== + +"@parcel/watcher-linux-arm-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152" + integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg== + +"@parcel/watcher-linux-arm64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809" + integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA== + +"@parcel/watcher-linux-arm64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4" + integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA== + +"@parcel/watcher-linux-x64-glibc@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639" + integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ== + +"@parcel/watcher-linux-x64-musl@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2" + integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg== + +"@parcel/watcher-win32-arm64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e" + integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q== + +"@parcel/watcher-win32-ia32@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d" + integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g== + +"@parcel/watcher-win32-x64@2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d" + integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw== + +"@parcel/watcher@^2.4.1": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.6.tgz#3f932828c894f06d0ad9cfefade1756ecc6ef1f1" + integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ== + dependencies: + detect-libc "^2.0.3" + is-glob "^4.0.3" + node-addon-api "^7.0.0" + picomatch "^4.0.3" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.6" + "@parcel/watcher-darwin-arm64" "2.5.6" + "@parcel/watcher-darwin-x64" "2.5.6" + "@parcel/watcher-freebsd-x64" "2.5.6" + "@parcel/watcher-linux-arm-glibc" "2.5.6" + "@parcel/watcher-linux-arm-musl" "2.5.6" + "@parcel/watcher-linux-arm64-glibc" "2.5.6" + "@parcel/watcher-linux-arm64-musl" "2.5.6" + "@parcel/watcher-linux-x64-glibc" "2.5.6" + "@parcel/watcher-linux-x64-musl" "2.5.6" + "@parcel/watcher-win32-arm64" "2.5.6" + "@parcel/watcher-win32-ia32" "2.5.6" + "@parcel/watcher-win32-x64" "2.5.6" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -4810,13 +4899,20 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@22.10.7", "@types/node@22.15.29": +"@types/node@*": version "22.15.29" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.29.tgz#c75999124a8224a3f79dd8b6ccfb37d74098f678" integrity sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ== dependencies: undici-types "~6.21.0" +"@types/node@22.10.7": + version "22.10.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.7.tgz#14a1ca33fd0ebdd9d63593ed8d3fbc882a6d28d7" + integrity sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg== + dependencies: + undici-types "~6.20.0" + "@types/pdf-parse@1.1.5": version "1.1.5" resolved "https://registry.yarnpkg.com/@types/pdf-parse/-/pdf-parse-1.1.5.tgz#a0959022604457169177622b512ed03b975f10e2" @@ -4854,7 +4950,7 @@ resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04" integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== -"@types/react-dom@*", "@types/react-dom@19.1.6": +"@types/react-dom@*": version "19.1.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.1.6.tgz#4af629da0e9f9c0f506fc4d1caa610399c595d64" integrity sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw== @@ -4866,7 +4962,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@19.1.6": +"@types/react@*": version "19.1.6" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.6.tgz#dee39f3e1e9a7d693f156a5840570b6d57f325ea" integrity sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q== @@ -4937,7 +5033,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@8.33.1": +"@typescript-eslint/eslint-plugin@*": version "8.33.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz#532641b416ed2afd5be893cddb2a58e9cd1f7a3e" integrity sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A== @@ -4952,7 +5048,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@*", "@typescript-eslint/parser@8.33.1": +"@typescript-eslint/parser@*": version "8.33.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.33.1.tgz#ef9a5ee6aa37a6b4f46cc36d08a14f828238afe2" integrity sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA== @@ -5723,6 +5819,11 @@ bail@^2.0.0: resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + balanced-match@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" @@ -5772,6 +5873,21 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1, brace-expansion@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + brace-expansion@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f" @@ -6026,6 +6142,13 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + chromatic@11.28.2: version "11.28.2" resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.28.2.tgz#c0b9f59dcc323ca8bf2c2790f3c9e3d285834efb" @@ -6193,6 +6316,11 @@ compute-scroll-into-view@^3.1.0: resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz#02c3386ec531fb6a9881967388e53e8564f3e9aa" integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -6506,7 +6634,7 @@ dequal@^2.0.0, dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -detect-libc@^2.1.2: +detect-libc@^2.0.3, detect-libc@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== @@ -7069,10 +7197,10 @@ eslint-visitor-keys@^4.2.0, eslint-visitor-keys@^4.2.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@*, eslint@^9.0.0: - version "9.39.2" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" - integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== +eslint@*: + version "9.39.3" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.3.tgz#08d63df1533d7743c0907b32a79a7e134e63ee2f" + integrity sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.1" @@ -7080,7 +7208,7 @@ eslint@*, eslint@^9.0.0: "@eslint/config-helpers" "^0.4.2" "@eslint/core" "^0.17.0" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.39.2" + "@eslint/js" "9.39.3" "@eslint/plugin-kit" "^0.4.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" @@ -8104,6 +8232,11 @@ ignore@^7.0.0, ignore@^7.0.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== +immutable@^5.0.2: + version "5.1.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.4.tgz#e3f8c1fe7b567d56cf26698f31918c241dae8c1f" + integrity sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA== + import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" @@ -9877,10 +10010,38 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@>=10.2.1, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^8.0.2, minimatch@^9.0.0, "minimatch@^9.0.3 || ^10.0.1", minimatch@^9.0.4, minimatch@^9.0.5: - version "10.2.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f" - integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw== +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^8.0.2: + version "8.0.7" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.7.tgz#954766e22da88a3e0a17ad93b58c15c9d8a579de" + integrity sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.0, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +"minimatch@^9.0.3 || ^10.0.1": + version "10.2.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" + integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg== dependencies: brace-expansion "^5.0.2" @@ -9965,6 +10126,11 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + node-ensure@^0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7" @@ -10964,6 +11130,11 @@ readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -11351,6 +11522,17 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sass@^1.97.3: + version "1.97.3" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.97.3.tgz#9cb59339514fa7e2aec592b9700953ac6e331ab2" + integrity sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg== + dependencies: + chokidar "^4.0.0" + immutable "^5.0.2" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + saxes@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" @@ -11590,7 +11772,7 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -12357,7 +12539,7 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript@*, typescript@5.8.3, typescript@^5.0.4: +typescript@*, typescript@^5.0.4: version "5.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== @@ -12380,6 +12562,11 @@ underscore.string@~3.3.6: sprintf-js "^1.1.1" util-deprecate "^1.0.2" +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== + undici-types@~6.21.0: version "6.21.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"