import React, { useState } from 'react'; import { Meta, Story } from '@storybook/react'; import { memori, tenant, history, historyWithMedia, historyWithAIGeneratedMessages, sessionID, dialogState as dialogStateWithHints, historyWithExpandable, historyWithArtifacts, } from '../../mocks/data'; import I18nWrapper from '../../I18nWrapper'; import Chat, { Props } from './Chat'; import './Chat.css'; import { ArtifactProvider } from '../MemoriArtifactSystem/context/ArtifactContext'; const meta: Meta = { title: 'Widget/Chat', component: Chat, argTypes: {}, parameters: { controls: { expanded: true }, }, }; export default meta; const dialogState = { ...dialogStateWithHints, hints: [], }; const Template: Story = args => { const [userMessage, setUserMessage] = useState(args.userMessage); return ( ); }; // By passing using the Args format for exported stories, you can control the props for a component for reuse in a test // https://storybook.js.org/docs/react/workflows/unit-testing export const Default = Template.bind({}); Default.args = { memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const MemoriTyping = Template.bind({}); MemoriTyping.args = { memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, memoriTyping: true, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithHints = Template.bind({}); WithHints.args = { memori, tenant, sessionID, history, dialogState: dialogStateWithHints, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithMessageConsumption = Template.bind({}); WithMessageConsumption.args = { memori, tenant, sessionID, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, showMessageConsumption: true, history: [ ...history.slice(0, Math.max(0, history.length - 2)), // Add one AI message with llmUsage for the badges { ...(history.find(m => !m.fromUser) ?? history[history.length - 1]), fromUser: false, text: 'Risposta di esempio con consumo LLM.', llmUsage: { provider: 'openai', model: 'gpt-4.1-mini', totalInputTokens: 123, inputCacheReadTokens: 10, inputCacheWriteTokens: 5, outputTokens: 456, durationMs: 2345, energyImpact: { energy: 0.00012, energyUnit: 'kWh', gwp: 0.00009, gwpUnit: 'kgCO2eq', wcf: 0.02, wcfUnit: 'L', }, }, } as any, ], }; export const WithArtifacts = Template.bind({}); WithArtifacts.args = { memori, tenant, sessionID, isChatlogPanel: false, history: historyWithArtifacts, dialogState, layout: 'CHAT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithArtifactsInChatlogPanel = Template.bind({}); WithArtifactsInChatlogPanel.args = { memori, tenant, sessionID, isChatlogPanel: true, history: historyWithArtifacts, dialogState, layout: 'CHAT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithMedia = Template.bind({}); WithMedia.args = { memori, tenant, sessionID, history: historyWithMedia, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; /** All media types in chat: one message from user, one from agent. Use for manual QA of previews and layout. */ const historyAllMediaExamples = [ { text: "Here's a message with every media type I can send.", fromUser: true, timestamp: '2021-03-01T12:00:00.000Z', media: [ { mediumID: 'user-img-1', mimeType: 'image/png', title: 'Image', url: 'https://picsum.photos/200/300', }, { mediumID: 'user-link-1', mimeType: 'text/html', title: 'Memori', url: 'https://memori.ai/en', }, { mediumID: 'user-code-1', mimeType: 'text/javascript', title: 'Code snippet', content: "console.log('Hello World!');", }, { mediumID: 'user-pdf-1', mimeType: 'application/pdf', title: 'PDF', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', }, { mediumID: 'user-video-1', mimeType: 'video/mp4', title: 'Video', url: 'https://www.w3schools.com/html/mov_bbb.mp4', }, { mediumID: 'user-audio-1', mimeType: 'audio/mpeg', title: 'Audio', url: 'https://www.w3schools.com/html/horse.mp3', }, { mediumID: 'user-doc-1', mimeType: 'application/msword', title: 'Word doc', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', }, ], }, { text: "And here's the same set of media from the agent.", fromUser: false, timestamp: '2021-03-01T12:00:01.000Z', media: [ { mediumID: 'agent-img-1', mimeType: 'image/png', title: 'Image', url: 'https://picsum.photos/300/200', }, { mediumID: 'agent-link-1', mimeType: 'text/html', title: 'YouTube', url: 'https://www.youtube.com/watch?v=feH26j3rBz8', }, { mediumID: 'agent-code-1', mimeType: 'application/json', title: 'JSON', content: '{"name": "Test", "value": 42}', }, { mediumID: 'agent-pdf-1', mimeType: 'application/pdf', title: 'PDF', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', }, { mediumID: 'agent-video-1', mimeType: 'video/mp4', title: 'Video', url: 'https://www.w3schools.com/html/mov_bbb.mp4', }, { mediumID: 'agent-audio-1', mimeType: 'audio/mpeg', title: 'Audio', url: 'https://www.w3schools.com/html/horse.mp3', }, { mediumID: 'agent-doc-1', mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', title: 'Excel', url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf', }, ], }, { text: 'This message has an inline document attachment: Hello from inline attachment.', fromUser: true, timestamp: '2021-03-01T12:00:02.000Z', }, ]; export const AllMediaExamples = Template.bind({}); AllMediaExamples.args = { memori, tenant, sessionID, history: historyAllMediaExamples, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; AllMediaExamples.storyName = 'All media examples (user + agent)'; export const WithInvalidImages = Template.bind({}); WithInvalidImages.args = { memori, tenant, sessionID, history: [ { text: 'Here are some images with invalid URLs to demonstrate fallback:', timestamp: '2021-03-01T12:00:00.000Z', media: [ { mediumID: 'invalid-1', mimeType: 'image/png', title: 'Invalid URL', url: 'not-a-valid-url', }, { mediumID: 'invalid-2', mimeType: 'image/jpeg', title: 'Invalid content', content: 'this-is-not-base64', }, { mediumID: 'invalid-3', mimeType: 'image/jpg', title: 'Missing URL and content', url: undefined, content: undefined, }, { mediumID: 'valid-1', mimeType: 'image/png', title: 'Valid image for comparison', url: 'https://picsum.photos/200/200', }, ], }, ], dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithDates = Template.bind({}); WithDates.args = { memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, showDates: true, }; export const WithContext = Template.bind({}); WithContext.args = { memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, showContextPerLine: true, }; export const WithDatesAndContext = Template.bind({}); WithDatesAndContext.args = { memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, showDates: true, showContextPerLine: true, }; export const OnX3State = Template.bind({}); OnX3State.args = { memori, tenant, sessionID, history, dialogState: { ...dialogState, state: 'X3', }, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const OnX2aState = Template.bind({}); OnX2aState.args = { memori, tenant, sessionID, history, dialogState: { ...dialogState, state: 'X2a', }, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const AcceptsFeedback = Template.bind({}); AcceptsFeedback.args = { memori, tenant, sessionID, history, dialogState: { ...dialogState, acceptsFeedback: true, }, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithAIGeneratedMessages = Template.bind({}); WithAIGeneratedMessages.args = { memori, tenant, sessionID, history: historyWithAIGeneratedMessages, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithUser = Template.bind({}); WithUser.args = { user: { avatarURL: 'https://picsum.photos/200' }, memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithCustomUserAvatar = Template.bind({}); WithCustomUserAvatar.args = { userAvatar: 'https://picsum.photos/200', memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithCustomUserAvatarAsElement = Template.bind({}); WithCustomUserAvatarAsElement.args = { userAvatar: USER, memori, tenant, sessionID, history, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithExpandable = Template.bind({}); WithExpandable.args = { memori, tenant, sessionID, history: historyWithExpandable, dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, }; export const WithMultipleArtifactsInOneMessage = Template.bind({}); WithMultipleArtifactsInOneMessage.args = { memori, tenant, sessionID, history: [ { text: 'Can you help me build a landing page?', fromUser: true, timestamp: '2021-03-01T12:00:00.000Z', }, { text: `I'll create a complete landing page with HTML, CSS, and JavaScript for you: Product Landing Page

Welcome to Our Amazing Product

The solution you've been waiting for

⚡ Fast

Lightning-quick performance

🔒 Secure

Enterprise-grade security

📱 Responsive

Works on all devices

* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', system-ui, sans-serif; line-height: 1.6; color: #333; } .hero { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-align: center; padding: 100px 20px; } .hero h1 { font-size: 3rem; margin-bottom: 20px; animation: fadeInUp 0.8s ease-out; } .hero p { font-size: 1.5rem; margin-bottom: 30px; opacity: 0.9; } .cta-button { background: white; color: #667eea; border: none; padding: 15px 40px; font-size: 1.1rem; font-weight: 600; border-radius: 50px; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); } .cta-button:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3); } .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 40px; padding: 80px 20px; max-width: 1200px; margin: 0 auto; } .feature { text-align: center; padding: 30px; background: white; border-radius: 10px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); transition: transform 0.3s ease; } .feature:hover { transform: translateY(-5px); } .feature h3 { font-size: 2rem; margin-bottom: 15px; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } // Add interactivity to the landing page document.addEventListener('DOMContentLoaded', () => { const ctaButton = document.getElementById('ctaButton'); if (ctaButton) { ctaButton.addEventListener('click', () => { // Smooth scroll animation const features = document.querySelector('.features'); if (features) { features.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Show a welcome message setTimeout(() => { alert('Welcome! Explore our amazing features below 🚀'); }, 500); }); // Add pulse animation on hover ctaButton.addEventListener('mouseenter', () => { ctaButton.style.animation = 'pulse 0.5s ease'; }); ctaButton.addEventListener('animationend', () => { ctaButton.style.animation = ''; }); } // Add scroll reveal effect to features const features = document.querySelectorAll('.feature'); const observerOptions = { threshold: 0.2, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach((entry, index) => { if (entry.isIntersecting) { setTimeout(() => { entry.target.style.opacity = '1'; entry.target.style.transform = 'translateY(0)'; }, index * 100); observer.unobserve(entry.target); } }); }, observerOptions); features.forEach(feature => { feature.style.opacity = '0'; feature.style.transform = 'translateY(20px)'; feature.style.transition = 'all 0.6s ease'; observer.observe(feature); }); }); // Add dynamic CSS for pulse animation const style = document.createElement('style'); style.textContent = \` @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } \`; document.head.appendChild(style); I've created three artifacts for you: 1. **HTML** - The page structure with a hero section and feature cards 2. **CSS** - Beautiful styling with gradients, animations, and responsive design 3. **JavaScript** - Interactive features including smooth scrolling and scroll animations Click on each card to view and customize the code!`, timestamp: '2021-03-01T12:01:00.000Z', }, ], dialogState, layout: 'DEFAULT', simulateUserPrompt: () => {}, sendMessage: (msg: string) => console.log(msg), stopListening: () => {}, resetTranscript: () => {}, setAttachmentsMenuOpen: () => {}, setSendOnEnter: () => {}, }; export const WithMultipleArtifactsInChatlogPanel = Template.bind({}); WithMultipleArtifactsInChatlogPanel.args = { memori, tenant, sessionID, isChatlogPanel: true, history: [ { text: 'Show me a React component with its styles and tests', fromUser: true, timestamp: '2021-03-01T12:00:00.000Z', }, { text: `Here's a complete React component setup with TypeScript, styles, and tests: import React from 'react'; import './Button.css'; interface ButtonProps { label: string; onClick?: () => void; variant?: 'primary' | 'secondary' | 'danger'; disabled?: boolean; } export const Button: React.FC = ({ label, onClick, variant = 'primary', disabled = false, }) => { return ( ); }; .btn { padding: 10px 20px; font-size: 16px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; transition: all 0.3s ease; font-family: inherit; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); } .btn-secondary { background: #6c757d; color: white; } .btn-secondary:hover:not(:disabled) { background: #5a6268; } .btn-danger { background: #dc3545; color: white; } .btn-danger:hover:not(:disabled) { background: #c82333; } import { render, screen, fireEvent } from '@testing-library/react'; import { Button } from './Button'; describe('Button Component', () => { it('renders with correct label', () => { render(