diff --git a/.changeset/stale-shrimps-lie.md b/.changeset/stale-shrimps-lie.md new file mode 100644 index 0000000000..6a1883f4bb --- /dev/null +++ b/.changeset/stale-shrimps-lie.md @@ -0,0 +1,6 @@ +--- +"nextjs-website": minor +"storybook-app": minor +--- + +Add chatbot chat history detail layout diff --git a/apps/nextjs-website/public/icons/chatbotChatUserBorder.svg b/apps/nextjs-website/public/icons/chatbotChatUserBorder.svg new file mode 100644 index 0000000000..0f0c5daf76 --- /dev/null +++ b/apps/nextjs-website/public/icons/chatbotChatUserBorder.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/nextjs-website/src/components/atoms/ChatHistoryMessage/ChatHistoryMessage.tsx b/apps/nextjs-website/src/components/atoms/ChatHistoryMessage/ChatHistoryMessage.tsx new file mode 100644 index 0000000000..c6cdb9c431 --- /dev/null +++ b/apps/nextjs-website/src/components/atoms/ChatHistoryMessage/ChatHistoryMessage.tsx @@ -0,0 +1,101 @@ +import { Stack, Typography, useTheme } from '@mui/material'; +import { defaultLocale } from '@/config'; +import IconWrapper from '@/components/atoms/IconWrapper/IconWrapper'; +import { parseChatMessage } from '@/helpers/chatMessageParser.helper'; + +type DateFormatOptions = { + locale?: string; + options?: Intl.DateTimeFormatOptions; +}; + +const DEFAULT_DATE_FORMAT = { + locale: defaultLocale, + options: { + timeStyle: 'short', + hourCycle: 'h23', + }, +} satisfies DateFormatOptions; + +type ChatMessageProps = { + text: string; + sender: string; + isQuestion: boolean; + timestamp?: string; +}; + +const ChatHistoryMessage = ({ + text, + timestamp, + isQuestion, + sender, +}: ChatMessageProps) => { + const { palette } = useTheme(); + const textColor = palette.text.primary; + const parsedChatMessage = parseChatMessage(text); + const iconSize = 28; + + const timeLabel = + timestamp && + new Intl.DateTimeFormat( + DEFAULT_DATE_FORMAT.locale, + DEFAULT_DATE_FORMAT.options + ).format(new Date(timestamp)); + + return ( + + + {isQuestion ? ( + + ) : ( + + )} + + {sender} + + {timeLabel && ( + + {timeLabel} + + )} + + + {parsedChatMessage} + + + ); +}; + +export default ChatHistoryMessage; diff --git a/apps/nextjs-website/src/components/atoms/ChatbotHistoryNavigationLink/ChatbotHistoryNavigationLink.tsx b/apps/nextjs-website/src/components/atoms/ChatbotHistoryNavigationLink/ChatbotHistoryNavigationLink.tsx new file mode 100644 index 0000000000..631782dd4f --- /dev/null +++ b/apps/nextjs-website/src/components/atoms/ChatbotHistoryNavigationLink/ChatbotHistoryNavigationLink.tsx @@ -0,0 +1,36 @@ +import { Link, useTheme } from '@mui/material'; + +type ChatbotHistoryNavigationLinkProps = { + sessionId: string; + sessionTitle: string; +}; + +const ChatbotHistoryNavigationLink = ({ + sessionId, + sessionTitle, +}: ChatbotHistoryNavigationLinkProps) => { + const { palette, typography } = useTheme(); + const textColor = palette.text.secondary; + + return ( + { + // TODO: Implement the navigation to the chatbot history session + console.log(`Navigating to chatbot history session: ${sessionId}`); + }} + > + {sessionTitle} + + ); +}; + +export default ChatbotHistoryNavigationLink; diff --git a/apps/nextjs-website/src/components/atoms/ChatbotHistoryNavigationMenu/ChatbotHistoryNavigationMenu.tsx b/apps/nextjs-website/src/components/atoms/ChatbotHistoryNavigationMenu/ChatbotHistoryNavigationMenu.tsx new file mode 100644 index 0000000000..cc7ab37121 --- /dev/null +++ b/apps/nextjs-website/src/components/atoms/ChatbotHistoryNavigationMenu/ChatbotHistoryNavigationMenu.tsx @@ -0,0 +1,58 @@ +import { ArrowBack, ArrowForward } from '@mui/icons-material'; +import { Stack, Typography, useTheme } from '@mui/material'; +import { useTranslations } from 'next-intl'; +import ChatbotHistoryNavigationLink from '@/components/atoms/ChatbotHistoryNavigationLink/ChatbotHistoryNavigationLink'; + +export type SessionNavigationData = { + sessionId: string; + sessionTitle: string; +}; + +type ChatbotHistoryNavigationMenuProps = { + previousSession?: SessionNavigationData; + nextSession?: SessionNavigationData; +}; + +const ChatbotHistoryNavigationMenu = ({ + previousSession, + nextSession, +}: ChatbotHistoryNavigationMenuProps) => { + const t = useTranslations(); + const { palette } = useTheme(); + const textColor = palette.text.secondary; + + return ( + + {previousSession && ( + + + {t('chatBot.previousChat')} + + + + + + + )} + {nextSession && ( + + + {t('chatBot.previousChat')} + + + + + + + )} + + ); +}; + +export default ChatbotHistoryNavigationMenu; diff --git a/apps/nextjs-website/src/components/molecules/ChatbotHistoryMessages/ChatbotHistoryMessages.tsx b/apps/nextjs-website/src/components/molecules/ChatbotHistoryMessages/ChatbotHistoryMessages.tsx new file mode 100644 index 0000000000..c93f4aa1c1 --- /dev/null +++ b/apps/nextjs-website/src/components/molecules/ChatbotHistoryMessages/ChatbotHistoryMessages.tsx @@ -0,0 +1,41 @@ +import ChatHistoryMessage from '@/components/atoms/ChatHistoryMessage/ChatHistoryMessage'; +import { Query } from '@/lib/chatbot/queries'; +import { Stack } from '@mui/material'; +import { useTranslations } from 'next-intl'; + +type ChatbotHistoryMessagesProps = { + queries: Query[]; + userName: string; +}; + +const ChatbotHistoryMessages = ({ + queries, + userName, +}: ChatbotHistoryMessagesProps) => { + const t = useTranslations(); + + return ( + + {queries.map((query) => ( + + + {query.answer && query.createdAt && ( + + )} + + ))} + + ); +}; + +export default ChatbotHistoryMessages; diff --git a/apps/nextjs-website/src/components/organisms/ChatbotHistoryDetailLayout/ChatbotHistoryDetailLayout.tsx b/apps/nextjs-website/src/components/organisms/ChatbotHistoryDetailLayout/ChatbotHistoryDetailLayout.tsx new file mode 100644 index 0000000000..d2ceac45db --- /dev/null +++ b/apps/nextjs-website/src/components/organisms/ChatbotHistoryDetailLayout/ChatbotHistoryDetailLayout.tsx @@ -0,0 +1,99 @@ +import ChatbotHistoryNavigationMenu, { + SessionNavigationData, +} from '@/components/atoms/ChatbotHistoryNavigationMenu/ChatbotHistoryNavigationMenu'; +import ChatbotHistoryMessages from '@/components/molecules/ChatbotHistoryMessages/ChatbotHistoryMessages'; +import { defaultLocale } from '@/config'; +import { Query } from '@/lib/chatbot/queries'; +import { Delete } from '@mui/icons-material'; +import { Box, Button, Stack, Typography, useTheme } from '@mui/material'; +import { useTranslations } from 'next-intl'; + +type DateFormatOptions = { + locale?: string; + options?: Intl.DateTimeFormatOptions; +}; + +const DEFAULT_DATE_FORMAT = { + locale: defaultLocale, + options: { + day: '2-digit', + month: 'long', + year: 'numeric', + }, +} satisfies DateFormatOptions; + +type ChatbotHistoryDetailLayoutProps = { + queries: Query[]; + userName: string; + previousSession?: SessionNavigationData; + nextSession?: SessionNavigationData; + onDeleteChatSession: (sessionId: string) => null; +}; + +const ChatbotHistoryDetailLayout = ({ + queries, + userName, + previousSession, + nextSession, + onDeleteChatSession, +}: ChatbotHistoryDetailLayoutProps) => { + const t = useTranslations(); + const { palette } = useTheme(); + + const date = new Intl.DateTimeFormat( + DEFAULT_DATE_FORMAT.locale, + DEFAULT_DATE_FORMAT.options + ).format(new Date(queries[0].queriedAt)); + + return ( + + {queries[0].question} + + + {date} + + + + + + + + + + + + ); +}; + +export default ChatbotHistoryDetailLayout; diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index 77fe6b6b72..08fa0e5503 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -604,6 +604,9 @@ "copied": "Copiato", "welcomeMessage": "**Benvenuto nel Dev Portal!**\n\nSono _Discovery_, il tuo assistente virtuale per la documentazione tecnica. Posso aiutarti con informazioni su API, guide, e altro.\n\n_Nota_: Discovery è in versione beta, quindi alcune risposte potrebbero non essere accurate. Verifica sempre le informazioni importanti con la documentazione ufficiale.\n\nCome posso aiutarti oggi?", "guestMessage": "Ciao sono _Discovery_ , il chatbot di DevPortal!\n\nPosso aiutarti a trovare in modo semplice e rapido le informazioni presenti nella documentazione del Portale.\n\nPer poter accedere al servizio, ti invito a [iscriverti a PagoPA DevPortal]({host}/auth/login)", + "deleteChat": "Elimina Chat", + "previousChat": "Chat Precedente", + "nextChat": "Chat Successiva", "errors": { "title": "Errore", "serviceDown": "Il chatbot al momento non è disponibile. Riprovare più tardi.", diff --git a/apps/storybook-app/public/icons/chatbotChatUserBorder.svg b/apps/storybook-app/public/icons/chatbotChatUserBorder.svg new file mode 100644 index 0000000000..0f0c5daf76 --- /dev/null +++ b/apps/storybook-app/public/icons/chatbotChatUserBorder.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/storybook-app/stories/atoms/ChatHistoryMessage.stories.tsx b/apps/storybook-app/stories/atoms/ChatHistoryMessage.stories.tsx new file mode 100644 index 0000000000..ea8a3bf0a2 --- /dev/null +++ b/apps/storybook-app/stories/atoms/ChatHistoryMessage.stories.tsx @@ -0,0 +1,41 @@ +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import ChatHistoryMessage from 'nextjs-website/src/components/atoms/ChatHistoryMessage/ChatHistoryMessage'; +import React from 'react'; +import { nextIntlContextDecorator } from '../next-intl-context.helper'; +import { mockText } from '../mock-content.helper'; + +const meta: Meta = { + title: 'Atoms/ChatHistoryMessage', + component: ChatHistoryMessage, +}; + +const decorator: Decorator = (story) => ( +
{story()}
+); + +export default meta; + +export const ChatBotMessage: StoryObj = { + args: { + text: ` + GPD gestisce i pagamenti spontanei attraverso il nodo dei pagamenti. + + Rif: + [PagoPA DevPortal | Overview delle componenti](https://developer.pagopa.it/pago-pa/guides/sanp/specifiche-attuative-del-nodo-dei-pagamenti-spc/funzionamento-generale/overview-delle-componenti) + `, + timestamp: '2024-07-24T17:14:07.129Z', + sender: 'Discovery', + isQuestion: false, + }, + decorators: [decorator, nextIntlContextDecorator], +}; + +export const UserMessage: StoryObj = { + args: { + text: mockText(23), + timestamp: '2024-07-24T17:14:08.129Z', + sender: 'John Doe', + isQuestion: true, + }, + decorators: [decorator, nextIntlContextDecorator], +}; diff --git a/apps/storybook-app/stories/atoms/ChatbotHistoryNavigationMenu.stories.tsx b/apps/storybook-app/stories/atoms/ChatbotHistoryNavigationMenu.stories.tsx new file mode 100644 index 0000000000..cc7b4da5c7 --- /dev/null +++ b/apps/storybook-app/stories/atoms/ChatbotHistoryNavigationMenu.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from '@storybook/react'; +import ChatbotHistoryNavigationMenu from '../../../nextjs-website/src/components/atoms/ChatbotHistoryNavigationMenu/ChatbotHistoryNavigationMenu'; +import { mockText } from '../mock-content.helper'; +import { nextIntlContextDecorator } from '../next-intl-context.helper'; + +const meta: Meta = { + title: 'Atoms/ChatbotHistoryNavigationMenu', + component: ChatbotHistoryNavigationMenu, +}; + +export default meta; + +export const Showcase: StoryObj = { + args: { + nextSession: { + sessionId: '1', + sessionTitle: mockText(10), + }, + previousSession: { + sessionId: '2', + sessionTitle: mockText(5), + }, + }, + decorators: [nextIntlContextDecorator], +}; diff --git a/apps/storybook-app/stories/fixtures/chatbotFixtures.tsx b/apps/storybook-app/stories/fixtures/chatbotFixtures.tsx new file mode 100644 index 0000000000..135ed4d903 --- /dev/null +++ b/apps/storybook-app/stories/fixtures/chatbotFixtures.tsx @@ -0,0 +1,100 @@ +import { mockText } from '../mock-content.helper'; + +export const sessionsList = [ + { + id: '111', + title: mockText(25), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + id: '123', + title: mockText(25), + createdAt: '2024-07-25T17:14:07.129Z', + }, + { + id: '456', + title: mockText(80), + createdAt: '2024-07-26T17:14:07.129Z', + }, + { + id: '789', + title: mockText(25), + createdAt: '2024-06-24T17:14:07.129Z', + }, + { + id: '321', + title: mockText(25), + createdAt: '2024-06-25T17:14:07.129Z', + }, + { + id: '654', + title: mockText(80), + createdAt: '2024-06-26T17:14:07.129Z', + }, + { + id: '987', + title: mockText(25), + createdAt: '2024-04-24T17:14:07.129Z', + }, + { + id: '543', + title: mockText(25), + createdAt: '2024-04-25T17:14:07.129Z', + }, + { + id: '333', + title: mockText(80), + createdAt: '2024-04-26T17:14:07.129Z', + }, + { + id: '1221', + title: mockText(25), + createdAt: '2024-03-24T17:14:07.129Z', + }, + { + id: '2352', + title: mockText(25), + createdAt: '2024-03-25T17:14:07.129Z', + }, + { + id: '45664', + title: mockText(80), + createdAt: '2024-03-26T17:14:07.129Z', + }, +]; + +export const chatbotChatSession = [ + { + sessionId: 'sessionID', + question: mockText(12), + queriedAt: '2024-07-24T17:14:07.129Z', + answer: mockText(24), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + sessionId: 'sessionID', + question: mockText(23), + queriedAt: '2024-07-24T17:14:07.129Z', + answer: mockText(44), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + sessionId: 'sessionID', + question: mockText(16), + queriedAt: '2024-07-24T17:14:07.129Z', + answer: mockText(30), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + sessionId: 'sessionID', + question: mockText(20), + queriedAt: '2024-07-24T17:14:07.129Z', + answer: mockText(24), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + sessionId: 'sessionID', + question: mockText(10), + queriedAt: '2024-07-24T17:14:07.129Z', + }, +]; diff --git a/apps/storybook-app/stories/molecules/ChatbotHistoryMessages.stories.tsx b/apps/storybook-app/stories/molecules/ChatbotHistoryMessages.stories.tsx new file mode 100644 index 0000000000..a33223c0cc --- /dev/null +++ b/apps/storybook-app/stories/molecules/ChatbotHistoryMessages.stories.tsx @@ -0,0 +1,47 @@ +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import ChatbotHistoryMessages from '../../../nextjs-website/src/components/molecules/ChatbotHistoryMessages/ChatbotHistoryMessages'; +import React from 'react'; +import { nextIntlContextDecorator } from '../next-intl-context.helper'; +import { mockText } from '../mock-content.helper'; + +const meta: Meta = { + title: 'Molecules/ChatbotHistoryMessages', + component: ChatbotHistoryMessages, +}; + +const decorator: Decorator = (story) => ( +
{story()}
+); + +export default meta; + +export const Showcase: StoryObj = { + args: { + queries: [ + { + id: '1', + sessionId: '1', + question: mockText(10), + queriedAt: '2024-07-24T17:14:07.129Z', + answer: mockText(80), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + id: '2', + sessionId: '1', + question: mockText(8), + queriedAt: '2024-07-24T17:14:07.129Z', + answer: mockText(30), + createdAt: '2024-07-24T17:14:07.129Z', + }, + { + id: '3', + sessionId: '1', + question: mockText(50), + queriedAt: '2024-07-24T17:14:07.129Z', + }, + ], + userName: 'John Doe', + }, + decorators: [decorator, nextIntlContextDecorator], +}; diff --git a/apps/storybook-app/stories/organisms/ChatbotHistoryDetailLayout.stories.tsx b/apps/storybook-app/stories/organisms/ChatbotHistoryDetailLayout.stories.tsx new file mode 100644 index 0000000000..91d2aeab1d --- /dev/null +++ b/apps/storybook-app/stories/organisms/ChatbotHistoryDetailLayout.stories.tsx @@ -0,0 +1,37 @@ +import { Decorator, Meta, StoryObj } from '@storybook/react'; +import ChatbotHistoryDetailLayout from 'nextjs-website/src/components/organisms/ChatbotHistoryDetailLayout/ChatbotHistoryDetailLayout'; +import { chatbotChatSession } from '../fixtures/chatbotFixtures'; +import React from 'react'; +import { mockText } from '../mock-content.helper'; +import { nextIntlContextDecorator } from '../next-intl-context.helper'; + +const meta: Meta = { + title: 'Organisms/ChatbotHistoryDetailLayout', + component: ChatbotHistoryDetailLayout, +}; + +const decorator: Decorator = (story) => ( +
{story()}
+); + +export default meta; + +export const Showcase: StoryObj = { + args: { + queries: chatbotChatSession, + userName: 'John Doe', + nextSession: { + sessionId: '1', + sessionTitle: mockText(10), + }, + previousSession: { + sessionId: '2', + sessionTitle: mockText(5), + }, + onDeleteChatSession: (sessionId: string) => { + console.log(sessionId); + return null; + }, + }, + decorators: [decorator, nextIntlContextDecorator], +};