================================================
FILE: src/pages/chat/chat.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React, { useEffect, useRef, useState } from 'react';
import { SupportPromptGroupProps } from '@cloudscape-design/chat-components/support-prompt-group';
import Alert from '@cloudscape-design/components/alert';
import Box from '@cloudscape-design/components/box';
import Container from '@cloudscape-design/components/container';
import ExpandableSection from '@cloudscape-design/components/expandable-section';
import FileDropzone, { useFilesDragging } from '@cloudscape-design/components/file-dropzone';
import FileInput from '@cloudscape-design/components/file-input';
import FileTokenGroup from '@cloudscape-design/components/file-token-group';
import Header from '@cloudscape-design/components/header';
import Icon from '@cloudscape-design/components/icon';
import Link from '@cloudscape-design/components/link';
import PromptInput from '@cloudscape-design/components/prompt-input';
import SpaceBetween from '@cloudscape-design/components/space-between';
import { isVisualRefresh } from '../../common/apply-mode';
import { FittedContainer, ScrollableContainer } from './common-components';
import {
fileTokenGroupI18nStrings,
getInitialMessages,
getInvalidPromptResponse,
getLoadingMessage,
Message,
responseList,
supportPromptItems,
supportPromptMessageOne,
supportPromptMessageTwo,
VALID_PROMPTS,
validLoadingPrompts,
} from './config';
import Messages from './messages';
import '../../styles/chat.scss';
export default function Chat() {
const waitTimeBeforeLoading = 300;
// The loading state will be shown for 4 seconds for loading prompt and 1.5 seconds for rest of the prompts
const waitTimeBeforeResponse = (isLoadingPrompt: boolean = false) => (isLoadingPrompt ? 4000 : 1500);
const [prompt, setPrompt] = useState('');
const [isGenAiResponseLoading, setIsGenAiResponseLoading] = useState(false);
const [showAlert, setShowAlert] = useState(true);
const messagesContainerRef = useRef<HTMLDivElement>(null);
const [files, setFiles] = useState<File[]>([]);
const promptInputRef = useRef<HTMLTextAreaElement>(null);
const [messages, setMessages] = useState<Message[]>([]);
const { areFilesDragging } = useFilesDragging();
const onSupportPromptClick = (detail: SupportPromptGroupProps.ItemClickDetail) => {
let newMessage: Message;
if (detail.id === 'typescript') {
newMessage = supportPromptMessageOne;
}
if (detail.id === 'expand') {
newMessage = supportPromptMessageTwo;
}
const supportPromptText = supportPromptItems.find(item => item.id === detail.id)?.text;
const newUserMessage: Message = {
type: 'chat-bubble',
authorId: 'user-jane-doe',
content: supportPromptText,
timestamp: new Date().toLocaleTimeString(),
};
setMessages(prevMessages => [...prevMessages, newUserMessage]);
promptInputRef.current?.focus();
setTimeout(() => {
setIsGenAiResponseLoading(true);
setMessages(prevMessages => [...prevMessages, getLoadingMessage()]);
setTimeout(() => {
setMessages(prevMessages => {
prevMessages.splice(prevMessages.length - 1, 1, newMessage);
return prevMessages;
});
setIsGenAiResponseLoading(false);
}, waitTimeBeforeResponse());
}, waitTimeBeforeLoading);
};
const setShowFeedbackDialog = (index: number, show: boolean) => {
setMessages(prevMessages => {
const updatedMessages = [...prevMessages];
const updatedMessage = { ...prevMessages[index], showFeedbackDialog: show };
updatedMessages.splice(index, 1, updatedMessage);
return updatedMessages;
});
};
const lastMessageContent = messages[messages.length - 1]?.content;
useEffect(() => {
setMessages(getInitialMessages(onSupportPromptClick));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
// Scroll to the bottom to show the new/latest message
setTimeout(() => {
if (messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
}
}, 0);
}, [lastMessageContent]);
const onPromptSend = ({ detail: { value } }: { detail: { value: string } }) => {
if ((!value && files.length === 0) || (value.length === 0 && files.length === 0) || isGenAiResponseLoading) {
return;
}
const newMessage: Message = {
type: 'chat-bubble',
authorId: 'user-jane-doe',
content: value,
timestamp: new Date().toLocaleTimeString(),
files,
};
let fileValue = files;
setMessages(prevMessages => [...prevMessages, newMessage]);
setPrompt('');
setFiles([]);
const lowerCasePrompt = value.toLowerCase();
const isLoadingPrompt = validLoadingPrompts.includes(lowerCasePrompt);
// Show loading state
setTimeout(() => {
setIsGenAiResponseLoading(true);
setMessages(prevMessages => [...prevMessages, getLoadingMessage()]);
setTimeout(() => {
const validPrompt =
fileValue.length > 0
? VALID_PROMPTS.find(({ prompt }) => prompt.includes('file'))
: VALID_PROMPTS.find(({ prompt }) => prompt.includes(lowerCasePrompt));
// Send Gen-AI response, replacing the loading chat bubble
setMessages(prevMessages => {
const response = validPrompt ? validPrompt.getResponse(onSupportPromptClick) : getInvalidPromptResponse();
prevMessages.splice(prevMessages.length - 1, 1, response);
return prevMessages;
});
setIsGenAiResponseLoading(false);
fileValue = [];
}, waitTimeBeforeResponse(isLoadingPrompt));
}, waitTimeBeforeLoading);
};
const addMessage = (index: number, message: Message) => {
setMessages(prevMessages => {
const updatedMessages = [...prevMessages];
updatedMessages.splice(index, 0, message);
return updatedMessages;
});
};
return (
<div className={`chat-container ${!isVisualRefresh && 'classic'}`}>
{showAlert && (
<Alert dismissible statusIconAriaLabel="Info" onDismiss={() => setShowAlert(false)}>
<ExpandableSection
variant="inline"
headerText="This demo showcases how to use generative AI components to build a generative AI chat. The interactions and
functionality are limited."
>
{responseList}
</ExpandableSection>
</Alert>
)}
<FittedContainer>
<Container
data-testid="chat-container"
header={<Header variant="h3">Generative AI chat</Header>}
fitHeight
disableContentPaddings
footer={
<>
{/* During loading, action button looks enabled but functionality is disabled. */}
{/* This will be fixed once prompt input receives an update where the action button can receive focus while being disabled. */}
{/* In the meantime, changing aria labels of prompt input and action button to reflect this. */}
<PromptInput
ref={promptInputRef}
onChange={({ detail }) => setPrompt(detail.value)}
onAction={onPromptSend}
value={prompt}
actionButtonAriaLabel={isGenAiResponseLoading ? 'Send message button - suppressed' : 'Send message'}
actionButtonIconName="send"
ariaLabel={isGenAiResponseLoading ? 'Prompt input - suppressed' : 'Prompt input'}
placeholder="Ask a question"
autoFocus
disableSecondaryActionsPaddings
secondaryActions={
<Box padding={{ left: 'xxs', top: 'xs' }}>
<FileInput
ariaLabel="Chat demo file input"
variant="icon"
multiple={true}
value={files}
onChange={({ detail }) => setFiles(prev => [...prev, ...detail.value])}
/>
</Box>
}
secondaryContent={
areFilesDragging ? (
<FileDropzone onChange={({ detail }) => setFiles(prev => [...prev, ...detail.value])}>
<SpaceBetween size="xs" alignItems="center">
<Icon name="upload" />
<Box>Drop files here</Box>
</SpaceBetween>
</FileDropzone>
) : (
files.length > 0 && (
<FileTokenGroup
items={files.map(file => ({ file }))}
onDismiss={({ detail }) => {
setFiles(files => files.filter((_, index) => index !== detail.fileIndex));
if (files.length === 1) {
promptInputRef.current?.focus();
}
}}
limit={3}
alignment="horizontal"
showFileThumbnail={true}
i18nStrings={fileTokenGroupI18nStrings}
/>
)
)
}
/>
<Box color="text-body-secondary" margin={{ top: 'xs' }} fontSize="body-s">
Use of this service is subject to the{' '}
<Link href="#" external variant="primary" fontSize="inherit">
AWS Responsible AI Policy
</Link>
.
</Box>
</>
}
>
<ScrollableContainer ref={messagesContainerRef}>
<Messages messages={messages} setShowFeedbackDialog={setShowFeedbackDialog} addMessage={addMessage} />
</ScrollableContainer>
</Container>
</FittedContainer>
</div>
);
}
================================================
FILE: src/pages/chat/common-components.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React, { forwardRef, useState } from 'react';
import Avatar from '@cloudscape-design/chat-components/avatar';
import ButtonGroup, { ButtonGroupProps } from '@cloudscape-design/components/button-group';
import StatusIndicator from '@cloudscape-design/components/status-indicator';
import { AuthorAvatarProps } from './config';
export function ChatBubbleAvatar({ type, name, initials, loading }: AuthorAvatarProps) {
if (type === 'gen-ai') {
return <Avatar color="gen-ai" iconName="gen-ai" tooltipText={name} ariaLabel={name} loading={loading} />;
}
return <Avatar initials={initials} tooltipText={name} ariaLabel={name} />;
}
export function CodeViewActions({ contentToCopy }: { contentToCopy: string }) {
return (
<ButtonGroup
ariaLabel="Code snippet actions"
variant="icon"
onItemClick={({ detail }) => {
if (detail.id !== 'copy' || !navigator.clipboard) {
return;
}
// eslint-disable-next-line no-console
navigator.clipboard.writeText(contentToCopy).catch(error => console.log('Failed to copy', error.message));
}}
items={[
{
type: 'group',
text: 'Feedback',
items: [
{
type: 'icon-button',
id: 'run-command',
iconName: 'play',
text: 'Run command',
},
{
type: 'icon-button',
id: 'send-cloudshell',
iconName: 'script',
text: 'Send to IDE',
},
],
},
{
type: 'icon-button',
id: 'copy',
iconName: 'copy',
text: 'Copy',
popoverFeedback: <StatusIndicator type="success">Message copied</StatusIndicator>,
},
]}
/>
);
}
export const FittedContainer = ({ children }: { children: React.ReactNode }) => {
return (
<div style={{ position: 'relative', flexGrow: 1 }}>
<div style={{ position: 'absolute', inset: 0 }}>{children}</div>
</div>
);
};
export const ScrollableContainer = forwardRef(function ScrollableContainer(
{ children }: { children: React.ReactNode },
ref: React.Ref<HTMLDivElement>,
) {
return (
<div style={{ position: 'relative', blockSize: '100%' }}>
<div style={{ position: 'absolute', inset: 0, overflowY: 'auto' }} ref={ref} data-testid="chat-scroll-container">
{children}
</div>
</div>
);
});
export function FeedbackActions({
contentToCopy,
onNotHelpfulFeedback,
}: {
contentToCopy: string;
onNotHelpfulFeedback: () => void;
}) {
const [feedback, setFeedback] = useState<string>('');
const [feedbackSubmitting, setFeedbackSubmitting] = useState<string>('');
const items: ButtonGroupProps.ItemOrGroup[] = [
{
type: 'group',
text: 'Vote',
items: [
{
type: 'icon-button',
id: 'helpful',
iconName: feedback === 'helpful' ? 'thumbs-up-filled' : 'thumbs-up',
text: 'Helpful',
disabled: !!feedback.length || feedbackSubmitting === 'not-helpful',
disabledReason: feedbackSubmitting.length
? ''
: feedback === 'helpful'
? '"Helpful" feedback has been submitted.'
: '"Helpful" option is unavailable after "Not helpful" feedback submitted.',
loading: feedbackSubmitting === 'helpful',
popoverFeedback:
feedback === 'helpful' ? (
<StatusIndicator type="success">Feedback submitted</StatusIndicator>
) : (
'Submitting feedback'
),
},
{
type: 'icon-button',
id: 'not-helpful',
iconName: feedback === 'not-helpful' ? 'thumbs-down-filled' : 'thumbs-down',
text: 'Not helpful',
disabled: !!feedback.length || feedbackSubmitting === 'helpful',
disabledReason: feedbackSubmitting.length
? ''
: feedback === 'not-helpful'
? '"Not helpful" feedback has been submitted.'
: '"Not helpful" option is unavailable after "Helpful" feedback submitted.',
loading: feedbackSubmitting === 'not-helpful',
popoverFeedback:
feedback === 'helpful' ? (
<StatusIndicator type="success">Feedback submitted</StatusIndicator>
) : (
'Submitting feedback'
),
},
],
},
{
type: 'icon-button',
id: 'copy',
iconName: 'copy',
text: 'Copy',
popoverFeedback: <StatusIndicator type="success">Message copied</StatusIndicator>,
},
];
return (
<ButtonGroup
ariaLabel="Chat actions"
variant="icon"
items={items}
onItemClick={({ detail }) => {
if (detail.id === 'copy' && navigator.clipboard) {
return (
navigator.clipboard
.writeText(contentToCopy)
// eslint-disable-next-line no-console
.catch(error => console.log('Failed to copy', error.message))
);
}
setFeedbackSubmitting(detail.id);
setTimeout(() => {
setFeedback(detail.id);
setFeedbackSubmitting('');
if (detail.id === 'not-helpful') {
onNotHelpfulFeedback();
}
}, 2000);
}}
/>
);
}
================================================
FILE: src/pages/chat/config.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React from 'react';
import SupportPromptGroup, { SupportPromptGroupProps } from '@cloudscape-design/chat-components/support-prompt-group';
import CodeView from '@cloudscape-design/code-view/code-view';
import typescriptHighlight from '@cloudscape-design/code-view/highlight/typescript';
import Box from '@cloudscape-design/components/box';
import CopyToClipboard from '@cloudscape-design/components/copy-to-clipboard';
import ExpandableSection from '@cloudscape-design/components/expandable-section';
import { FileTokenGroupProps } from '@cloudscape-design/components/file-token-group';
import Link from '@cloudscape-design/components/link';
import Popover from '@cloudscape-design/components/popover';
import SpaceBetween from '@cloudscape-design/components/space-between';
import TextContent from '@cloudscape-design/components/text-content';
export type Message = ChatBubbleMessage | AlertMessage;
type ChatBubbleMessage = {
type: 'chat-bubble';
authorId: string;
content: React.ReactNode;
timestamp: string;
actions?: 'feedback' | 'code-view';
hideAvatar?: boolean;
avatarLoading?: boolean;
files?: File[];
supportPrompts?: React.ReactNode;
showFeedbackDialog?: boolean;
contentToCopy?: string;
};
type AlertMessage = {
type: 'alert';
content: React.ReactNode;
header?: string;
};
export const supportPromptItems = [
{
text: 'What else can I do with TypeScript?',
id: 'typescript',
},
{
text: 'How would I add parameters and type checking to this function?',
id: 'expand',
},
];
export const responseList = (
<TextContent>
<ol>
<li>To see how an incoming response from generative AI is displayed, ask "Show a loading state example".</li>
<li>To see an error alert that appears when something goes wrong, ask "Show an error state example".</li>
<li>To see a how a file upload is displayed, upload one or more files.</li>
<li>To see support prompts, ask "Show support prompts".</li>
</ol>
</TextContent>
);
// added as function so that timestamp is evaluated when function is called
export const getInvalidPromptResponse = (): Message => ({
type: 'chat-bubble',
authorId: 'gen-ai',
content: (
<>
The interactions and functionality of this demo are limited.
{responseList}
</>
),
timestamp: new Date().toLocaleTimeString(),
actions: 'feedback',
contentToCopy: `The interactions and functionality of this demo are limited.
1. To see how an incoming response from generative AI is displayed, ask "Show a loading state example".
2. To see an error alert that appears when something goes wrong, ask "Show an error state example".
3. To see a how a file upload is displayed, upload one or more files.
4. To see support prompts, ask "Show support prompts".`,
});
export const getLoadingMessage = (): Message => ({
type: 'chat-bubble',
authorId: 'gen-ai',
content: <Box color="text-status-inactive">Generating a response</Box>,
timestamp: new Date().toLocaleTimeString(),
avatarLoading: true,
});
const getFileResponseMessage = (): Message => ({
type: 'chat-bubble',
authorId: 'gen-ai',
content:
'I see you have uploaded one or more files. I cannot parse the files right now, but you can see what uploaded files look like.',
timestamp: new Date().toLocaleTimeString(),
avatarLoading: false,
actions: 'feedback',
contentToCopy:
'I see you have uploaded one or more files. I cannot parse the files right now, but you can see what uploaded files look like.',
});
const getLoadingStateResponseMessage = (): Message => ({
type: 'chat-bubble',
authorId: 'gen-ai',
content: 'That was the loading state. To see the loading state again, ask "Show a loading state example".',
timestamp: new Date().toLocaleTimeString(),
avatarLoading: false,
actions: 'feedback',
contentToCopy: 'That was the loading state. To see the loading state again, ask "Show a loading state example".',
});
const getErrorStateResponseMessage = (): Message => ({
type: 'alert',
header: 'Access denied',
content: (
<SpaceBetween size="s">
<span>
You don't have permission to [AWSS3:ListBuckets]. To request access, copy the following text and send it to your
AWS administrator.{' '}
<Link href="#" external variant="primary">
Learn more about troubleshooting access denied errors.
</Link>
</span>
<div className="access-denied-alert-wrapper">
<div className="access-denied-alert-wrapper__box">
<SpaceBetween size="xxxs">
<Box variant="code">
<div>User: [arn:aws:iam::123456789000:user/awsgenericuser]</div>
<div>Service: [AWSS3]</div>
<div>Action: [ListBuckets]</div>
<div>On resource(s): [arn:aws:S3:us-east-1:09876543211234567890]</div>
<div>Context: [no identity-based policy allows the AWSS3:ListBuckets action.]</div>
</Box>
</SpaceBetween>
</div>
<div>
<CopyToClipboard
copyButtonText="Copy"
copyErrorText="Text failed to copy"
copySuccessText="Text copied"
textToCopy={`User: [arn:aws:iam::123456789000:user/awsgenericuser]
Service: [AWSS3]
Action: [ListBuckets]
On resource(s): [arn:aws:S3:us-east-1:09876543211234567890]
Context: [no identity-based policy allows the AWSS3:ListBuckets action.]
`}
/>
</div>
</div>
</SpaceBetween>
),
});
const getSupportPromptResponseMessage = (
onSupportPromptClick?: (detail: SupportPromptGroupProps.ItemClickDetail) => void,
): Message => ({
type: 'chat-bubble',
authorId: 'gen-ai',
content: (
<CodeView
content={`// This is the main function that will be executed when the script runs
function main(): void {
// Use console.log to print "Hello, World!" to the console
console.log("Hello, World!");
}
// Call the main function to execute the program
main();`}
highlight={typescriptHighlight}
/>
),
actions: 'code-view',
contentToCopy: `// This is the main function that will be executed when the script runs
function main(): void {
// Use console.log to print "Hello, World!" to the console
console.log("Hello, World!");
}
// Call the main function to execute the program
main();`,
timestamp: new Date().toLocaleTimeString(),
supportPrompts: (
<SupportPromptGroup
ariaLabel="Proposed prompts"
items={supportPromptItems}
onItemClick={({ detail }) => {
onSupportPromptClick?.(detail);
}}
/>
),
});
type ValidPromptType = {
prompt: Array<string>;
getResponse: (onSupportPromptClick?: (detail: SupportPromptGroupProps.ItemClickDetail) => void) => Message;
};
export const validLoadingPrompts = ['show a loading state example', 'loading state', 'loading'];
export const VALID_PROMPTS: Array<ValidPromptType> = [
{
prompt: validLoadingPrompts,
getResponse: getLoadingStateResponseMessage,
},
{
prompt: ['show an error state example', 'error state', 'error'],
getResponse: getErrorStateResponseMessage,
},
{
prompt: ['file'],
getResponse: getFileResponseMessage,
},
{
prompt: ['show support prompts', 'support prompts', 'support prompt'],
getResponse: onSupportPromptClick => getSupportPromptResponseMessage(onSupportPromptClick),
},
];
// Needed only for the existing messages upon page load.
function getTimestampMinutesAgo(minutesAgo: number) {
const d = new Date();
d.setMinutes(d.getMinutes() - minutesAgo);
return d.toLocaleTimeString();
}
export type AuthorAvatarProps = {
type: 'user' | 'gen-ai';
name: string;
initials?: string;
loading?: boolean;
};
type AuthorsType = {
[key: string]: AuthorAvatarProps;
};
export const AUTHORS: AuthorsType = {
'user-jane-doe': { type: 'user', name: 'Jane Doe', initials: 'JD' },
'gen-ai': { type: 'gen-ai', name: 'Generative AI assistant' },
};
const CitationPopover = ({ count, href }: { count: number; href: string }) => (
<Box color="text-status-info" display="inline">
<Popover
header="Source"
content={
<Link href={href} external variant="primary">
{href}
</Link>
}
position="right"
>
[{count}]
</Popover>
</Box>
);
export const getInitialMessages = (
onSupportPromptClick: (detail: SupportPromptGroupProps.ItemClickDetail) => void,
): Array<Message> => {
return [
{
type: 'chat-bubble',
authorId: 'user-jane-doe',
content: 'What can I do with Amazon S3?',
timestamp: getTimestampMinutesAgo(10),
},
{
type: 'chat-bubble',
authorId: 'gen-ai',
content:
'Amazon S3 provides a simple web service interface that you can use to store and retrieve any amount of data, at any time, from anywhere. Using this service, you can easily build applications that make use of cloud native storage. Since Amazon S3 is highly scalable and you only pay for what you use, you can start small and grow your application as you wish, with no compromise on performance or reliability.',
timestamp: getTimestampMinutesAgo(9),
actions: 'feedback',
contentToCopy:
'Amazon S3 provides a simple web service interface that you can use to store and retrieve any amount of data, at any time, from anywhere. Using this service, you can easily build applications that make use of cloud native storage. Since Amazon S3 is highly scalable and you only pay for what you use, you can start small and grow your application as you wish, with no compromise on performance or reliability.',
},
{
type: 'chat-bubble',
authorId: 'user-jane-doe',
content: 'How can I create an S3 bucket configuration?',
timestamp: getTimestampMinutesAgo(8),
},
{
type: 'chat-bubble',
authorId: 'gen-ai',
content: (
<TextContent>
Creating a configuration for Amazon S3 involves setting up a bucket and configuring its properties{' '}
<CitationPopover
count={1}
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html"
/>
. Here's a step-by-step guide to help you create an S3 configuration:
<ol>
<li>Sign in to AWS Management Console</li>
<li>Access Amazon S3 console</li>
<li>Create a new S3 bucket</li>
<li>
Configure bucket settings{' '}
<CitationPopover
count={2}
href="https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile-S3-source.html"
/>
</li>
<li>Review and create</li>
</ol>
<Box padding={{ top: 'xs' }}>
<ExpandableSection headerText="Sources">
<div>
<Link
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html"
external
variant="primary"
>
[1] Getting started with Amazon S3 - Amazon Simple Storage Service
</Link>
</div>
<div>
<Link
href="https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-configuration-and-profile-S3-source.html"
external
variant="primary"
>
[2] Understanding configurations stored in Amazon S3 - AWS AppConfig
</Link>
</div>
<div>
<Link
href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html"
external
variant="primary"
>
[3] Tutorial: Configuring a static website on Amazon S3 - Amazon Simple Storage Service
</Link>
</div>
</ExpandableSection>
</Box>
</TextContent>
),
timestamp: getTimestampMinutesAgo(7),
actions: 'feedback',
contentToCopy: `Creating a configuration for Amazon S3 involves setting up a bucket and configuring its properties. Here's a step-by-step guide to help you create an S3 configuration:
1. Sign in to AWS Management Console
2. Access Amazon S3 console
3. Create a new S3 bucket
4. Configure bucket settings
5. Review and create`,
},
{
type: 'chat-bubble',
authorId: 'user-jane-doe',
content: 'Give me an example of a Typescript code block.',
timestamp: getTimestampMinutesAgo(6),
},
{
type: 'chat-bubble',
authorId: 'gen-ai',
content: "Here's a simple TypeScript code example that implements the 'Hello, World!' functionality:",
timestamp: getTimestampMinutesAgo(5),
actions: 'feedback',
contentToCopy: "Here's a simple TypeScript code example that implements the 'Hello, World!' functionality:",
},
{
type: 'chat-bubble',
authorId: 'gen-ai',
content: (
<CodeView
content={`// This is the main function that will be executed when the script runs
function main(): void {
// Use console.log to print "Hello, World!" to the console
console.log("Hello, World!");
}
// Call the main function to execute the program
main();`}
highlight={typescriptHighlight}
/>
),
actions: 'code-view',
contentToCopy: `// This is the main function that will be executed when the script runs
function main(): void {
// Use console.log to print "Hello, World!" to the console
console.log("Hello, World!");
}
// Call the main function to execute the program
main();`,
timestamp: getTimestampMinutesAgo(4),
hideAvatar: true,
supportPrompts: (
<SupportPromptGroup
ariaLabel="Typescript support prompt group"
items={supportPromptItems}
onItemClick={({ detail }) => {
onSupportPromptClick(detail);
}}
/>
),
},
];
};
export const supportPromptMessageOne: Message = {
type: 'chat-bubble',
authorId: 'gen-ai',
content: (
<>
TypeScript is a powerful programming language that builds upon JavaScript by adding static typing and other
features. Here are key things you can do with TypeScript:
<ol>
<li>
Web developement
<ul>
<li>Build frontend applications using frameworks like Angular, React, or Vue.js</li>
<li>Create robust server-side applications with Node.js</li>
<li>Develop full-stack applications with enhanced type safety</li>
</ul>
</li>
<li>
Type safety features
<ul>
<li>Define explicit types for variables, functions, and objects</li>
<li>Catch errors during development before runtime</li>
<li>Use interfaces and type declarations for better code organization</li>
</ul>
</li>
<li>
Object-oriented programming
<ul>
<li>Create classes with proper inheritance</li>
<li>Implement interfaces</li>
<li>Use access modifiers (public, private, protected)</li>
</ul>
</li>
</ol>
TypeScript is particularly valuable for large projects where type safety and code maintainability are important
considerations.
</>
),
timestamp: new Date().toLocaleTimeString(),
actions: 'feedback',
contentToCopy: `TypeScript is a powerful programming language that builds upon JavaScript by adding static typing and other features. Here are key things you can do with TypeScript:
1. Web developement
- Build frontend applications using frameworks like Angular, React, or Vue.js
- Create robust server-side applications with Node.js
- Develop full-stack applications with enhanced type safety
2. Type safety features
- Define explicit types for variables, functions, and objects
- Catch errors during development before runtime
- Use interfaces and type declarations for better code organization
3. Object-oriented programming
- Create classes with proper inheritance
- Implement interfaces
- Use access modifiers (public, private, protected)
TypeScript is particularly valuable for large projects where type safety and code maintainability are important considerations.`,
};
export const supportPromptMessageTwo: Message = {
type: 'chat-bubble',
authorId: 'gen-ai',
content: (
<CodeView
highlight={typescriptHighlight}
content={`// Here's how you might add input parameters and type checking
function enhancedMain(name: string, greeting: string = "Hello"): void {
if (!name) {
throw new error('Name parameter is required.');
}
console.log(\`\${greeting}, \${name}!\`);
}
// Call the enhancedMain function to execute the program
enhancedMain('Greetings', 'Earth');`}
/>
),
actions: 'code-view',
contentToCopy: `// Add input parameters and type checking
function enhancedMain(name: string, greeting: string = "Hello"): void {
if (!name) {
throw new error('Name parameter is required.');
}
console.log("{greeting}, {name}!");
}
// Call the enhancedMain function to execute the program
enhancedMain('Greetings', 'Earth');`,
timestamp: new Date().toLocaleTimeString(),
};
export const fileTokenGroupI18nStrings: FileTokenGroupProps.I18nStrings = {
removeFileAriaLabel: index => `Remove file ${index + 1}`,
limitShowFewer: 'Show fewer files',
limitShowMore: 'Show more files',
errorIconAriaLabel: 'Error',
warningIconAriaLabel: 'Warning',
};
================================================
FILE: src/pages/chat/index.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React from 'react';
import { createRoot } from 'react-dom/client';
import { I18nProvider } from '@cloudscape-design/components/i18n';
import enMessages from '@cloudscape-design/components/i18n/messages/all.en.json';
import { CustomAppLayout, Notifications } from '../commons/common-components';
import Chat from './chat';
import '../../styles/base.scss';
function App() {
return (
<I18nProvider locale="en" messages={[enMessages]}>
<CustomAppLayout
maxContentWidth={1280}
toolsHide
navigationHide
content={<Chat />}
notifications={<Notifications />}
/>
</I18nProvider>
);
}
createRoot(document.getElementById('app')!).render(<App />);
================================================
FILE: src/pages/chat/messages.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React from 'react';
import ChatBubble from '@cloudscape-design/chat-components/chat-bubble';
import Alert from '@cloudscape-design/components/alert';
import FileTokenGroup from '@cloudscape-design/components/file-token-group';
import LiveRegion from '@cloudscape-design/components/live-region';
import SpaceBetween from '@cloudscape-design/components/space-between';
import FeedbackDialog from './additional-info/feedback-dialog';
import { ChatBubbleAvatar, CodeViewActions, FeedbackActions } from './common-components';
import { AUTHORS, fileTokenGroupI18nStrings, Message, supportPromptItems } from './config';
import '../../styles/chat.scss';
export default function Messages({
messages = [],
setShowFeedbackDialog,
addMessage,
}: {
messages: Array<Message>;
setShowFeedbackDialog: (index: number, show: boolean) => void;
addMessage: (index: number, message: Message) => void;
}) {
const latestMessage: Message = messages[messages.length - 1];
const promptText = supportPromptItems.map(item => item.text);
return (
<div className="messages" role="region" aria-label="Chat">
<LiveRegion hidden={true} assertive={latestMessage?.type === 'alert'}>
{latestMessage?.type === 'alert' && latestMessage.header}
{latestMessage?.content}
{latestMessage?.type === 'chat-bubble' &&
latestMessage.supportPrompts &&
`There are ${promptText.length} support prompts accompanying this message. ${promptText}`}
</LiveRegion>
{messages.map((message, index) => {
if (message.type === 'alert') {
return (
<Alert
key={'error-alert' + index}
header={message.header}
type="error"
statusIconAriaLabel="Error"
data-testid={'error-alert' + index}
>
{message.content}
</Alert>
);
}
const author = AUTHORS[message.authorId];
return (
<SpaceBetween size="xs" key={message.authorId + message.timestamp}>
<ChatBubble
avatar={<ChatBubbleAvatar {...author} loading={message.avatarLoading} />}
ariaLabel={`${author.name} at ${message.timestamp}`}
type={author.type === 'gen-ai' ? 'incoming' : 'outgoing'}
hideAvatar={message.hideAvatar}
actions={
message.actions === 'code-view' ? (
<CodeViewActions contentToCopy={message.contentToCopy || ''} />
) : message.actions === 'feedback' ? (
<FeedbackActions
contentToCopy={message.contentToCopy || ''}
onNotHelpfulFeedback={() => setShowFeedbackDialog(index, true)}
/>
) : null
}
>
<SpaceBetween size="xs">
<div key={message.authorId + message.timestamp + 'content'}>{message.content}</div>
{message.files && message.files.length > 0 && (
<FileTokenGroup
readOnly
items={message.files.map(file => ({ file }))}
limit={3}
onDismiss={() => {
/* empty function for read only token */
}}
alignment="horizontal"
showFileThumbnail={true}
i18nStrings={fileTokenGroupI18nStrings}
/>
)}
</SpaceBetween>
</ChatBubble>
{message.showFeedbackDialog && (
<div className="other-content-vertically-align">
<FeedbackDialog
onDismiss={() => setShowFeedbackDialog(index, false)}
onSubmit={() => {
setShowFeedbackDialog(index, false);
addMessage(index + 1, {
type: 'chat-bubble',
authorId: 'gen-ai',
content: 'Your feedback has been submitted. Thank you for your additional feedback.',
timestamp: new Date().toLocaleTimeString(),
hideAvatar: true,
});
}}
/>
</div>
)}
{latestMessage.type === 'chat-bubble' && latestMessage.supportPrompts && index === messages.length - 1 && (
<div className="other-content-vertically-align">{message.supportPrompts}</div>
)}
</SpaceBetween>
);
})}
</div>
);
}
================================================
FILE: src/pages/chat/additional-info/dialog.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React from 'react';
import Button from '@cloudscape-design/components/button';
import '../../../styles/dialog.scss';
const Dialog = React.forwardRef(
(
{
children,
footer,
onDismiss,
ariaLabel,
}: {
children: React.ReactNode;
footer: React.ReactNode;
onDismiss: () => void;
ariaLabel: string;
},
ref: React.Ref<HTMLDivElement>,
) => {
return (
<div
ref={ref}
className="dialog"
role="dialog"
aria-label={ariaLabel}
aria-modal="false" // Maintains natural focus flow since it's an inline dialog
tabIndex={-1} // This allows the dialog to receive focus
>
<div className="content">
<div className="dismiss">
<Button
iconName="close"
variant="icon"
onClick={onDismiss}
ariaLabel="Close dialog"
data-testid="dialog-dismiss-button"
/>
</div>
<div className="inner-content">{children}</div>
</div>
<div className="footer">{footer}</div>
</div>
);
},
);
Dialog.displayName = 'Dialog';
export default Dialog;
================================================
FILE: src/pages/chat/additional-info/feedback-dialog.tsx
================================================
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
import React, { useEffect, useRef } from 'react';
import Box from '@cloudscape-design/components/box';
import Button from '@cloudscape-design/components/button';
import Checkbox from '@cloudscape-design/components/checkbox';
import Form from '@cloudscape-design/components/form';
import FormField from '@cloudscape-design/components/form-field';
import SpaceBetween from '@cloudscape-design/components/space-between';
import Textarea from '@cloudscape-design/components/textarea';
import Dialog from './dialog';
export default function FeedbackDialog({ onDismiss, onSubmit }: { onDismiss: () => void; onSubmit: () => void }) {
const [feedbackOptions, setFeedbackOptions] = React.useState({
harmful: false,
incomplete: false,
inaccurate: false,
other: false,
});
const [feedbackText, setFeedbackText] = React.useState('');
const dialogRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
// Store the element that had focus before dialog opened
triggerRef.current = document.activeElement as HTMLElement;
// Focus the dialog container when it opens
dialogRef.current?.focus();
// Cleanup: Return focus to the trigger element when dialog closes
return () => {
triggerRef.current?.focus();
};
}, [dialogRef]);
const selectOption = (option: string, checked: boolean) =>
setFeedbackOptions({ ...feedbackOptions, [option]: checked });
const isFeedbackOptionSelected = Object.values(feedbackOptions).some(val => !!val);
const isSubmittable = isFeedbackOptionSelected || feedbackText.length > 0;
const submitFeedback = () => {
if (!isSubmittable) {
return;
}
onSubmit();
};
return (
<Dialog
ariaLabel="Feedback dialog"
ref={dialogRef}
onDismiss={onDismiss}
footer={
<div style={{ display: 'flex', flexDirection: 'row-reverse', gap: '4px' }}>
<Button
data-testid="feedback-submit-button"
onClick={submitFeedback}
ariaLabel="Submit form"
disabled={!isSubmittable}
>
Submit
</Button>
<Button variant="link" onClick={onDismiss} ariaLabel="Close dialog" data-testid="feedback-close-button">
Close
</Button>
</div>
}
>
<Form
header={
<Box variant="h4">
Tell us more - <i>optional</i>
</Box>
}
>
<SpaceBetween direction="vertical" size="l">
<FormField label="What did you dislike about the response?">
<SpaceBetween size="l" direction="horizontal">
<Checkbox
data-testid="feedback-checkbox-harmful"
checked={feedbackOptions.harmful}
onChange={({ detail }) => selectOption('harmful', detail.checked)}
>
Harmful
</Checkbox>
<Checkbox
checked={feedbackOptions.incomplete}
onChange={({ detail }) => selectOption('incomplete', detail.checked)}
>
Incomplete
</Checkbox>
<Checkbox
checked={feedbackOptions.inaccurate}
onChange={({ detail }) => selectOption('inaccurate', detail.checked)}
>
Inaccurate
</Checkbox>
<Checkbox
checked={feedbackOptions.other}
onChange={({ detail }) => selectOption('other', detail.checked)}
>
Other
</Checkbox>
</SpaceBetween>
</FormField>
<FormField label="Additional notes" stretch={true}>
<Textarea
rows={3}
onChange={({ detail }) => setFeedbackText(detail.value)}
value={feedbackText}
placeholder={'Additional feedback'}
/>
</FormField>
</SpaceBetween>
</Form>
</Dialog>
);
}