Experimental: The AI extension and its component APIs may change in the future. Please give us feedback on your experience with them.
Import#
import { Response } from '@dnb/eufemia/extensions/ai'import '@dnb/eufemia/extensions/ai/style'render(<Response />)
Description#
Images are disabled by default. Set allowedImagePrefixes to trusted origins when your application needs them. Ambiguous single-dot amounts such as 11.725 NOK stay as written in Norwegian and Swedish; Danish dot grouping remains supported.
Ai.Response renders markdown from an AI model with Eufemia elements, such as P, Heading, Lists, Code, Anchor and Table. It has no extra dependencies.
It is made for streaming:
- Unfinished markdown in the last block, like
**boldor an open code span, is closed while it streams in. Unfinished links and images are hidden until they are complete. - Blocks that are finished are not rendered again when new text arrives.
It is safe by default:
- Raw HTML is never rendered, only shown as text.
- Links and images with unsafe URLs (like
javascript:) are rendered as text. UseallowedLinkPrefixesandallowedImagePrefixesto only allow known URLs.
Supported markdown: headings, paragraphs, emphasis, strikethrough, inline code, fenced code blocks, ordered, unordered and task lists, blockquotes, thematic breaks, tables, links, autolinks and images.
Code blocks use CopyOnClick.Button to copy the code with a mouse, touch, or keyboard. The button keeps its translated "Copy code" label and announces the translated confirmation in a tooltip only when copying succeeds.
Currency amounts#
Recognized currency amounts use NumberFormat.Currency automatically, including in emphasis, lists and table cells. They inherit the Provider's display locale and NumberFormat settings, including the currency display. With Norwegian defaults, 1 234 kr is displayed as 1 234 kr. The number of written decimals is preserved. Formatting does not verify whether an AI-generated amount is correct.
Supported uppercase codes are NOK, SEK, DKK, ISK, EUR, USD, GBP, CHF, JPY, CAD, AUD, NZD, CNY, HKD, SGD, INR and PLN, before or after an amount. € identifies EUR and £ identifies GBP. kr uses the Provider currency only when it is NOK, SEK, DKK or ISK. Bare numbers, $, unsupported markers, malformed amounts and values that would lose numeric precision remain unchanged. Code, link labels, URLs, image alternatives and HTML attributes are not formatted.
The source locale controls decimal and grouping separators. It defaults to the Provider locale. Standard three-digit grouping with Unicode space separators and the locale's grouping separator is supported. Dot grouping is also supported for comma-decimal locales. Amounts with more than two fractional digits remain text.
Recognition is conservative. A prefix amount followed by a word stays text, including across emphasis. This preserves expressions such as NOK 1,5 milliarder, EUR 1,5 millones and ordinary prose such as NOK 150 betales nå. without trying to interpret the following word. A complete prefix amount such as NOK 150. or a suffix amount such as 150 kr betales nå. can be formatted. Recognition stays within a single text segment, so **100 NOK** works while **100** NOK stays text. This is a limited text recognizer, not a financial expression parser.
If the generated text uses another locale, set currencyFormatting={{ locale: 'en-GB' }}. You can also provide currency to interpret kr, for example currencyFormatting={{ locale: 'sv-SE', currency: 'SEK' }}. An invalid source locale leaves amounts unchanged. Set currencyFormatting={false} to preserve all original currency text.
While streaming, an amount at the end of unfinished text remains unformatted until following content confirms its boundary. Whitespace alone is not enough because it could be a grouping separator. Set parseIncompleteMarkdown={false} when streaming finishes, stops or fails so a final amount can be formatted. Keep this setting specific to each message.
When the application already knows the amount and currency, render NumberFormat.Currency directly from those values. Prefer this for financial content rather than extracting values from generated prose. See the structured amount demo.
Not supported: syntax highlighting, math, diagrams, indented code blocks, HTML and HTML entities. You can add syntax highlighting by overriding the code element with components. Code blocks get a language-* class.
Horizontal table overflow#
Markdown tables use Table.ScrollView. If a table is wider than its available space, a translated hint below it explains that it can be scrolled horizontally. The hint is associated with the scroll area for assistive technology and disappears when all columns fit. Keyboard scrolling remains available. The hint also updates when streaming changes the table or the container resizes.
| Key | nb-NO | en-GB | sv-SE | da-DK |
|---|---|---|---|---|
tableOverflow | Rull vannrett for å se hele tabellen | Scroll horizontally to see the full table | Rulla i sidled för att se hela tabellen | Rul vandret for at se hele tabellen |
Demos#
Response#
Your spending in March
You spent , which is more less than last month.
- Groceries:
4 200 kr - Transport:
1 100 kr- Bus
- Train
Tip: Set up a savings agreement at www.dnb.no.
| Category | Amount |
|---|---|
| Groceries | 4 200 |
| Transport | 1 100 |
const total = groceries + transport<Response>{answer}</Response>
Wide tables#
Tables retain horizontal scrolling and keyboard access. A translated hint appears below the table only when some columns are outside the visible area. Resize the example to see the hint appear or disappear.
| Category | This month | Last month | Difference |
|---|---|---|---|
| Groceries | |||
| Transport |
<div style={{ maxWidth: '18rem', }} > <Response parseIncompleteMarkdown={false}> {[ '| Category | This month | Last month | Difference |', '| :-- | --: | --: | --: |', '| Groceries | 4 200 kr | 3 900 kr | 300 kr |', '| Transport | 1 100 kr | 1 300 kr | -200 kr |', ].join('\n')} </Response> </div>
Streaming response#
Unfinished markdown is closed while the text streams in. Finished blocks are not rendered again.
const StreamingResponse = () => { const [length, setLength] = useState(0) useEffect(() => { if (length >= answer.length) { return // stop here } const timeout = setTimeout( () => setLength((current) => current + 3), 30 ) return () => clearTimeout(timeout) }, [length]) return ( <> <Button variant="secondary" bottom onClick={() => setLength(0)}> Replay </Button> <Response>{answer.slice(0, length)}</Response> </> ) } render(<StreamingResponse />)
Formatted currency amounts#
Explicit currencies in text, emphasis and table cells use NumberFormat.Currency. Bare numbers and ambiguous symbols remain text.
Prefix amounts followed by a word also remain text. This includes scaled amounts in any language and ordinary prose such as NOK 150 betales nå. Suffix amounts such as 150 kr betales nå. can still be formatted.
You spent and received a refund of .
| Item | Amount |
|---|---|
| Travel | |
| Books |
Bare numbers like 12345 and ambiguous symbols like $100 stay as text.
Prefix amounts followed by words, such as EUR 1,5 millones and NOK 150 payable today, also stay as text.
<Provider locale="nb-NO"> <Response parseIncompleteMarkdown={false}>{currencyResponse}</Response> </Provider>
Source locale#
Set the source locale when the response uses different separators from the display locale. This example reads English amounts and displays them in Norwegian.
The total is ; the fee is .
<Provider locale="nb-NO"> <Response parseIncompleteMarkdown={false} currencyFormatting={{ locale: 'en-GB', }} > {'The total is USD 1,234.50; the fee is EUR 99.95.'} </Response> </Provider>
Structured amount#
When the application knows the amount and currency, render NumberFormat.Currency directly. This example uses a numeric value from structured data alongside generated prose, without extracting or interpreting the amount from the text.
<Provider locale="nb-NO"> <Message from="assistant"> <Response currencyFormatting={false}> {'The confirmed amount is:'} </Response> <P> <NumberFormat.Currency value={responseAmount.value} currency={responseAmount.currency} decimals={0} /> </P> </Message> </Provider>
Streaming currency amounts#
An amount stays as text until following content confirms its boundary or streaming completes.
const StreamingAmount = () => { const text = 'Your balance is NOK 12 400,50. The refund is 250,00 kr.' const [length, setLength] = useState(0) useEffect(() => { if (length >= text.length) { return undefined } const timeout = setTimeout(() => setLength(length + 1), 100) return () => clearTimeout(timeout) }, [length, text]) return ( <Provider locale="nb-NO"> <Button variant="secondary" bottom onClick={() => setLength(0)}> Replay </Button> <Response parseIncompleteMarkdown={length < text.length}> {text.slice(0, length)} </Response> </Provider> ) } render(<StreamingAmount />)
Images require a trusted origin#
Images render as alternative text by default, without an image request. Applications can allow trusted image origins explicitly.
Untrusted image
<Response> {''} </Response>
Allowed URLs#
Links and images that are not allowed are rendered as text.
Read more on dnb.no, not on example.com or this.
<Response allowedLinkPrefixes={['https://www.dnb.no']}> { 'Read more on [dnb.no](https://www.dnb.no/), not on [example.com](https://example.com) or [this](javascript:alert(1)).' } </Response>
Custom elements#
Use components to override how a markdown element is rendered.
This answer uses a custom element for bold text.
const Strong = ({ children }) => ( <strong style={{ color: 'var(--color-sea-green)', }} > {children} </strong> ) render( <Response components={{ strong: Strong, }} > {'This answer uses a **custom** element for **bold** text.'} </Response> )