Experimental: The AI extension and its component APIs may change in the future. Please give us feedback on your experience with them.
Examples#
Full chat experience#
A complete chat with an assistant. Choose a suggestion or write a message, e.g. "Block my card" or "Compare my spending", to see the tools, answer and sources stream in. Press stop to end the reply, or regenerate the last answer.
The demo uses scripted replies and local chat state. Connect your own messages, status, send, stop and regenerate actions to the components in your app.
New chat opens an empty conversation and clears any draft or pending reply. The demo content and component labels are in English. See Translations for changing component labels; your application supplies translated chat content.
DNB Banking Assistant
Where do you want to start today?
const primarySuggestions = [ { icon: card, prompt: 'Block my Visa card', }, { icon: bank, prompt: 'Compare my spending with last month', }, { icon: transfer, prompt: 'Show my latest transactions', }, ] const suggestionChips = [ { icon: loan, label: 'loan offers', prompt: 'What loan offers do you have?', }, { label: 'what can you do?', prompt: 'What can you help me with?', }, ] const ainoAvatar = <Avatar size="small" icon={aino} hasLabel /> const BankingAssistant = () => { const [chatStarted, setChatStarted] = useState(false) const [draft, setDraft] = useState('') const promptRef = useRef<HTMLTextAreaElement>(null) const [feedback, setFeedback] = useState({}) const { messages, status, sendMessage, stop, regenerate, setMessages } = useChatSimulation() const send = (text) => sendMessage({ text, }) const questions = messages.filter(({ role }) => role === 'user') return ( <Provider locale="en-GB"> <ChatLayout> <section className="chat" style={chatStyle}> <Flex.Horizontal align="center" justify="space-between"> <Flex.Horizontal align="center" gap="small"> {ainoAvatar} <div> <strong>Aino</strong> <P size="small">DNB Banking Assistant</P> </div> </Flex.Horizontal> <Button variant="tertiary" icon={add} iconPosition="left" onClick={() => { setMessages([]) setFeedback({}) setDraft('') setChatStarted(true) promptRef.current?.focus() }} > New chat </Button> </Flex.Horizontal> {!chatStarted && messages.length === 0 ? ( <Welcome greeting="Hello, Peter" avatar={<Avatar size="small" icon={aino} hasLabel />} title="Where do you want to start today?" style={{ margin: 'auto 0', }} > <Suggestions> {primarySuggestions.map(({ icon, prompt }) => ( <Suggestion key={prompt} icon={icon} suggestion={prompt} onClick={({ suggestion }) => { send(suggestion) promptRef.current?.focus() }} /> ))} </Suggestions> <Suggestions> {suggestionChips.map(({ icon, label, prompt }) => ( <Suggestion key={label} icon={icon} suggestion={prompt} onClick={({ suggestion }) => { send(suggestion) promptRef.current?.focus() }} > {label} </Suggestion> ))} </Suggestions> </Welcome> ) : ( <Conversation id="banking-chat" aria-live="off" scrollBehavior="end" style={{ flex: '1 1 auto', }} > <DateMarker date={new Date()} /> <Info> <P> Aino is a chatbot for customer service. Do not share personal information with the chatbot. Read more about{' '} <Anchor href="https://www.dnb.no/personvern"> your privacy </Anchor> . </P> <P top> Some answers are generated with artificial intelligence and are marked with a tag. AI-generated answers can contain errors. </P> </Info> {messages.map((message, index) => { const isAssistant = message.role === 'assistant' const isLast = index === messages.length - 1 const isDone = !(isLast && status === 'streaming') const transactions = getTransactions(message) const actions = isAssistant && isDone && ( <Actions> <CopyOnClick.Button size="small" iconSize="default" bounding copyContent={getText(message)} title="Copy response" tooltip="Copy response" /> <Action icon={thumbs_up} label="Good answer" selected={feedback[message.id] === true} onClick={() => setFeedback((all) => ({ ...all, [message.id]: all[message.id] === true ? undefined : true, })) } /> <Action icon={thumbs_down} label="Bad answer" selected={feedback[message.id] === false} onClick={() => setFeedback((all) => ({ ...all, [message.id]: all[message.id] === false ? undefined : false, })) } /> {isLast && ( <Action icon={refresh} label="Regenerate" onClick={() => { regenerate() promptRef.current?.focus() }} /> )} </Actions> ) return ( <Fragment key={message.id}> <Message id={message.id} from={message.role} aria-label={isAssistant ? 'Aino' : 'You'} grouped={ isAssistant && messages[index - 1]?.role === 'assistant' } avatar={isAssistant ? ainoAvatar : undefined} aiGenerated={isAssistant} actions={transactions ? undefined : actions} > <DemoMessageContent message={message} /> {message.stopped && ( <P size="small">Reply stopped.</P> )} </Message> {transactions && ( <Message variant="plain" actions={actions}> <List.Container> {transactions.map( ({ name, account, date, amount }) => ( <List.Item.Action key={name} icon={amount < 0 ? transfer : pay_from} onClick={() => send( `Show the payment confirmation for ${name}` ) } > <List.Cell.Title> <List.Cell.Title.Overline> {account} · {date} </List.Cell.Title.Overline> {name} </List.Cell.Title> <List.Cell.End> <NumberFormat.Currency value={amount} /> </List.Cell.End> </List.Item.Action> ) )} </List.Container> </Message> )} </Fragment> ) })} {status === 'submitted' && ( <MessageIndicator name="Aino" avatar={ainoAvatar} /> )} </Conversation> )} <ReplyAnnouncement isBusy={status !== 'ready'} messageId={ messages.at(-1)?.role === 'assistant' ? messages.at(-1).id : undefined } outcome={ messages.at(-1)?.stopped ? 'Reply stopped.' : undefined } /> <PromptInput value={draft} textareaProps={{ ref: promptRef, }} onChange={({ value }) => setDraft(value)} isBusy={status === 'submitted' || status === 'streaming'} characterCounter={200} onSubmit={({ value }) => { send(value) setDraft('') }} onStop={stop} /> </section> {questions.length > 1 && ( <QuestionOverview questions={questions} /> )} </ChatLayout> </Provider> ) } // Jump between the questions, and see which one you read const QuestionOverview = ({ questions }) => { const { scrollToMessage } = useConversation('banking-chat') const { currentTurnId } = useConversationVisibility('banking-chat') return ( <nav aria-label="Questions in this chat"> <P bottom="x-small"> <strong>Questions</strong> </P> <List.Container> {questions.map((question) => ( <List.Item.Action key={question.id} selected={question.id === currentTurnId} aria-current={ question.id === currentTurnId ? 'step' : undefined } onClick={() => scrollToMessage(question.id)} > <List.Cell.Title>{getText(question)}</List.Cell.Title> </List.Item.Action> ))} </List.Container> </nav> ) } render(<BankingAssistant />)
Chat window and customer-service handover#
Compose a chat window with Dialog for its keyboard focus, close controls and responsive modal behavior. The input uses the large radius token inside the window. Suggestions sit above the input; the application owns sending them. A customer-service reply uses the assistant side with a headset avatar and no AI-generated label.
<Dialog restoreFocusTo={() => document.getElementById('ai-chat-window-trigger')} trigger={(props) => ( <Button {...props} id="ai-chat-window-trigger"> Open chat window </Button> )} minWidth="23rem" maxWidth="23rem" title="Chat with DNB" > <Flex.Stack gap="small" style={{ height: 'min(38rem, 65dvh)', minHeight: '18rem', }} > <Conversation scrollBehavior="end" aria-live="off" style={{ flex: '1 1 auto', }} > <Message from="user">Can I speak to a person?</Message> <Message aria-label="Aino" avatar={<Avatar size="small" icon={aino} hasLabel />} > I will connect you to customer service. </Message> <Message aria-label="Maria, customer service" avatar={<Avatar size="small" icon={customer_service} hasLabel />} > Hello, I am Maria from DNB. How can I help? </Message> </Conversation> <Suggestions aria-label="Suggested replies"> <Suggestion suggestion="Help with a payment" /> <Suggestion suggestion="Help with my card" /> </Suggestions> <PromptInput style={ { '--ai-prompt-input-radius': 'var(--token-radius-lg)', } as React.CSSProperties } /> </Flex.Stack> </Dialog>
Streamed reply announcements#
The application keeps a separate AriaLive region mounted and updates it once when the reply ends, including when Stop preserves a partial answer. The conversation's automatic live announcements are disabled to avoid duplicates. See Screen reader announcements for the application helper and failure handling.
const Chat = () => { const { messages, status, sendMessage, stop } = useChatSimulation() const isBusy = status !== 'ready' const reply = messages.at(-1) return ( <> <Conversation aria-live="off" style={{ height: '18rem', }} > {messages.map((message) => ( <Message key={message.id} id={message.id} from={message.role}> <DemoMessageContent message={message} /> {message.stopped && <P size="small">Reply stopped.</P>} </Message> ))} </Conversation> <ReplyAnnouncement isBusy={isBusy} messageId={reply?.role === 'assistant' ? reply.id : undefined} outcome={reply?.stopped ? 'Reply stopped.' : undefined} /> <PromptInput isBusy={isBusy} onSubmit={({ value }) => { sendMessage({ text: value, }) }} onStop={() => { stop() }} /> </> ) } render(<Chat />)
Chat with compact input#
Where do you want to start today?
<Welcome greeting="Hello, Peter" avatar={<Avatar size="small" icon={aino} hasLabel />} title="Where do you want to start today?" > <Suggestions> <Suggestion icon={bank} suggestion="Explain my spending this month" /> <Suggestion icon={bank} suggestion="Show my savings goals" /> <Suggestion icon={bank} suggestion="When is my next bill due?" /> </Suggestions> <PromptInput variant="compact" placeholder="Start typing here" /> </Welcome>
Chat with content in the conversation#
User messages are placed on the right. Content like a list can be shown without a bubble by using variant="plain".
<div style={chatStyle}> <Conversation style={{ flex: '1 1 auto', }} > <Message from="user" name="You" timestamp="15:23" avatar={<Avatar hasLabel>P</Avatar>} > Hi, I need the payment confirmation for a transfer I made from my salary account 12.02 </Message> <Message name="Aino" timestamp="15:24" avatar={ <Avatar variant="secondary" hasLabel> A </Avatar> } > Hi, it seems you made 2 transfers 12.02. I have provided both for you below. Just click the correct one and you will be able to get to the payment confirmation. </Message> <Message variant="plain"> <List.Container> <List.Item.Action icon={transfer}> <List.Cell.Title> <List.Cell.Title.Overline> (xxxx.xx.xxxxx) </List.Cell.Title.Overline> Intro Aksel </List.Cell.Title> <List.Cell.End> <NumberFormat.Currency value={-888888} /> </List.Cell.End> </List.Item.Action> <List.Item.Action icon={pay_from}> <List.Cell.Title> <List.Cell.Title.Overline> (xxxx.xx.xxxxx) </List.Cell.Title.Overline> Kim Olsen </List.Cell.Title> <List.Cell.End> <NumberFormat.Currency value={-888888} /> </List.Cell.End> </List.Item.Action> </List.Container> </Message> </Conversation> <PromptInput characterCounter={111} /> </div>