Styling Your LLMClient’s Chat

Every LLMClient comes with a chat window: in the Smarter workbench, where you test it, and on every web page that embeds it with Smarter Chat. Out of the box, that window is a tidy blue and white. With a few lines in your manifest, it can wear your brand: your logo in the corner, your colors on the message bubbles, your font, a picture behind the conversation, and a friendly face beside each of the assistant’s answers.

This page is written for prompt engineers. You don’t need to be a web developer, and you don’t need to know CSS (the language that web pages use for their looks). Everything you need is explained here, with examples you can copy.

Where the Styles Go

An LLMClient’s manifest has two parts under spec.config:

  • prompt (required) is what the LLMClient is: its LLM provider and model, its system prompt, and the words of its chat, such as its name, welcome message, example prompts and placeholder.

  • styles (optional) is how its chat looks.

apiVersion: smarter.sh/v1
kind: LLMClient
metadata:
  name: stackademy_sql
  description: Stackademy's course advisor.
  version: 1.0.0
spec:
  config:
    prompt:
      provider: openai
      defaultModel: gpt-6-luna
      appName: Stackademy
      appAssistant: Stanley
      appWelcomeMessage: Welcome to Stackademy! How can I help you today?
    styles:
      appLogoUrl: https://cdn.example.com/stackademy/logo.png
      outgoingMessage:
        backgroundColor: "#fca311"

That’s a complete styling: a logo, and the user’s messages in Stackademy’s amber. Every setting in styles is optional. Anything you leave out keeps its default look, so you can start with one line and add more as you go.

Apply the manifest as usual, then reload the LLMClient’s chat in the workbench to see the result:

smarter apply -f stackademy.yaml

The Parts of the Chat

The chat window is made of a few parts, and styles has a section for each. The section names are the names that the chat’s building blocks use (see Under the Hood), so they’re the same ones you’ll see in the chat’s technical documentation.

S
Stackademy v1.0.0
openai gpt-6-luna
⭯
Welcome to Stackademy! How can I help you today?
Do you offer any courses on AI?
••• Stanley is typing
Ask me anything about Stackademy courses...
➤
mainContainerthe frame around everything conversationHeaderthe dark bar at the top, with appLogoUrl in its circle messageListthe striped area that holds the conversation, and appBackgroundImageUrl incomingMessagethe assistant's gray bubble, with assistantAvatarUrl beside it outgoingMessagethe user's amber bubble, with userAvatarUrl beside it typingIndicator"Stanley is typing", while the LLM answers messageInputthe box at the bottom, where the user types buttonsthe send arrow, the attach button, and the header's buttons

A Two-Minute CSS Crash Course

Styles are written as CSS values, the same values that every web page uses. You only need four kinds.

Colors

There are three ways to write a color. Use whichever your brand guide gives you.

Kind

Example

Notes

Hex code

"#fca311"

The most common. A # and six digits or letters (0-9, a-f): two each for red, green and blue. "#ffffff" is white, "#000000" is black. Short forms like "#fff" work too. Put hex colors in quotes, because in YAML a # starts a comment.

Color name

tomato

Browsers know about 140 color names, such as navy, gold, white and tomato. See the list of names.

rgb / rgba

rgba(255, 255, 255, 0.8)

Red, green and blue from 0 to 255. The a version adds opacity from 0 (invisible) to 1 (solid), so 0.8 is 80% solid, and lets what’s behind it show through a little. hsl() and hsla() colors work too.

Any color picker shows a color’s hex code. Try Google’s color picker, or your design tool’s.

Sizes

A size is a number and a unit, with no space between them:

  • px (pixels) is a fixed size: 16px is a typical text size.

  • em and rem are relative to the text size: 1em is one text-height, 0.5em half of one. Sizes in em grow and shrink with the text.

  • 0 needs no unit.

Fonts

A font stack is a list of fonts, separated by commas. The visitor’s browser uses the first one that it has, so a stack ends with a generic family: sans-serif, serif or monospace.

fontFamily: '"Georgia", "Times New Roman", serif'

Most visitors don’t have your brand font installed, so the chat can download one for them, from the free Google Fonts catalog. Find a font there, and copy its name exactly:

googleFont: Nunito

That’s all: when fontFamily isn’t set, the chat uses the Google font, with a sensible stack behind it.

Images

An image is a web address (url) that starts with https:// and ends at the image itself, usually .png, .jpg, .svg or .webp. Paste it into a browser’s address bar: if you see the image and nothing else, it’s the right url. A link to a page that shows the image, such as a Google Drive or Dropbox sharing page, won’t work. Host images somewhere public, such as your website, a CDN, or a public bucket.

  • Logo and avatars are shown in small circles, about 42 pixels wide. Use a square image, at least 128 by 128 pixels, with its subject in the middle. A logo is fitted whole into a white circle. An avatar fills its circle, so its corners are cut off.

  • Background images sit behind the conversation. Choose a quiet image or a light pattern, so that the messages stay easy to read, and keep it under about 500 KB, so that the chat loads quickly.

When you apply the manifest, Smarter opens each image url, as a browser would, and refuses the manifest if one doesn’t answer with an image: a typo, an image that hasn’t been uploaded yet, a private file, or a link to a web page instead of the image itself. A url that forwards to the image (a redirect) is fine.

If an image disappears later, after it was applied, the chat leaves it out rather than show a broken picture. It just won’t show your image.

Every Setting

All settings are optional.

The overall look

Setting

What it does

Example

appLogoUrl

Your logo, in a circle at the left of the header. It’s also shown on the LLMClient’s card in the web console.

https://cdn.example.com/logo.png

appBackgroundImageUrl

A picture behind the conversation. See Background Images.

https://cdn.example.com/bg.png

appBackgroundImageSize

How the picture fills the area: cover (the default) fills it and crops the picture’s edges, contain shows the whole picture, and auto repeats it at its own size, like tiles. Use auto for a pattern.

auto

assistantAvatarUrl

A small picture beside each of the assistant’s messages: a mascot, a face, or your logo.

https://cdn.example.com/stanley.png

userAvatarUrl

A small picture beside each of the user’s messages.

https://cdn.example.com/user.png

googleFont

A font from Google Fonts, which the chat downloads.

Nunito

fontFamily

The chat’s font stack. It wins over googleFont, so use it to put a Google font in a stack of your own.

'"Nunito", Arial, sans-serif'

fontSize

The size of the chat’s text. Every other size grows and shrinks with it. The default is 16px.

15px

borderRadius

How round the corners of the message bubbles and of the message box are. 0 is square, and the default is 0.7em.

16px

The parts of the chat

Each part is a section of styles, with its settings indented beneath it:

styles:
  conversationHeader:
    backgroundColor: "#14213d"
    color: "#ffffff"

Section

Settings

What they color

mainContainer

backgroundColor, color, borderColor

The frame around the whole chat, its default text color, and the thin line around it.

conversationHeader

backgroundColor, color, infoColor, borderColor

The bar at the top: its background, the LLMClient’s name (color), the line beneath the name, which names the LLM provider and model (infoColor), and the line under the bar.

messageList

backgroundColor, color

The area that holds the conversation. Over a background image, backgroundColor tints the image. See Background Images.

incomingMessage

backgroundColor, color, borderColor, linkColor

The assistant’s bubbles: their color, their text, an optional thin line around them, and the links in them.

outgoingMessage

backgroundColor, color, borderColor, linkColor

The user’s bubbles, likewise.

messageInput

backgroundColor, editorBackgroundColor, color, placeholderColor, borderColor

The strip at the bottom (backgroundColor), the box itself (editorBackgroundColor), what the user types (color), the hint in the empty box (placeholderColor, whose words are spec.config.prompt.appPlaceholder), and the line above the strip.

buttons

color

The send and attach buttons, and the header’s buttons.

typingIndicator

color

“Stanley is typing” and its bouncing dots.

Background Images

A background image sits behind the conversation, and messageList.backgroundColor is laid over it like tinted glass. A solid color would hide the image completely, so use an rgba color with an opacity below 1:

styles:
  appBackgroundImageUrl: https://cdn.example.com/stackademy/pattern.png
  appBackgroundImageSize: auto              # a pattern, repeated like tiles
  messageList:
    backgroundColor: rgba(255, 255, 255, 0.85)   # 85% white, over the pattern

The higher the opacity, the fainter the image, and the easier the messages are to read. Between 0.75 and 0.9 usually looks good. For a dark chat, tint with a dark color instead, such as rgba(20, 33, 61, 0.9).

Case Study: Stackademy

Stackademy is an online coding school, the fictional company behind several of Smarter’s examples. Its course advisor, an LLMClient named stackademy_sql, answers questions about the course catalog. It works well, but on Stackademy’s website it looks like every other chat. The marketing team sends over the brand guide:

  • Colors: Oxford navy #14213d, amber #fca311, platinum gray #e5e5e5, and white.

  • Font: Nunito, a rounded, friendly sans-serif.

  • Logo: a square PNG, and a mascot, Stanley, the friendly owl who answers students’ questions.

  • Tone: welcoming, a little playful, never childish.

Here’s how a prompt engineer turns that into styles, one step at a time, applying the manifest after each step and reloading the workbench to see the result.

Step 1: put the images online

The logo, Stanley’s portrait and a faint pattern of code symbols go on Stackademy’s CDN. Each is checked by pasting its url into a browser.

Step 3: the bubbles

Students’ own messages are amber, and Stanley’s are platinum gray, both with navy text. Navy on amber is easy to read; white on amber, the first idea, was too faint. Links in Stanley’s answers are a clear blue, and Stanley’s portrait goes beside them.

assistantAvatarUrl: https://cdn.stackademy.edu/brand/stanley.png
incomingMessage:
  backgroundColor: "#e5e5e5"
  color: "#14213d"
  linkColor: "#0b57d0"
outgoingMessage:
  backgroundColor: "#fca311"
  color: "#14213d"

Step 4: the font, the corners, and the background

Nunito comes from Google Fonts. Rounder corners suit its friendly letters. The code-symbol pattern is tiled behind the conversation, and tinted 85% white so that it whispers rather than shouts.

googleFont: Nunito
fontSize: 15px
borderRadius: 16px
appBackgroundImageUrl: https://cdn.stackademy.edu/brand/pattern.png
appBackgroundImageSize: auto
messageList:
  backgroundColor: rgba(255, 255, 255, 0.85)
  color: "#14213d"

Step 5: the finishing touches

The send button and the typing indicator pick up the amber, and the message box is a soft gray, so that it reads as a place to type.

messageInput:
  editorBackgroundColor: "#f1f1f1"
  placeholderColor: "#6c757d"
buttons:
  color: "#fca311"
typingIndicator:
  color: "#fca311"

Step 6: check, and ship

Before deploying, the prompt engineer checks three things:

  1. Can everyone read it? Each text color is checked against its background with WebAIM’s free contrast checker. Aim for its “WCAG AA: Pass”.

  2. Does it work on a phone? The workbench is resized to a phone’s width.

  3. Does it survive a long answer? A question that produces a table, a code block and a list.

Then the LLMClient is deployed, and the very same look appears on Stackademy’s website, which embeds it with Smarter Chat. No web developer was needed: the next time marketing changes a color, the prompt engineer changes one line and applies the manifest again.

The complete manifest is one of Smarter’s built-in examples. In it, Stackademy’s images are on Smarter’s own CDN, rather than Stackademy’s:

stackademy-llmclient-sql.yaml
apiVersion: smarter.sh/v1
kind: LLMClient
metadata:
  name: stackademy_sql
  description: Stackademy University course catalogue inquiries using the Stackademy SQL plugin.
  version: 1.0.0
  tags:
    - stackademy
    - sql
    - llmclient
  annotations:
    - smarter.sh/llm-client/stackademy-sql/creator: QA Team
    - smarter.sh/llm-client/stackademy-sql/purpose: llmclient for Stackademy course catalogue inquiries
    - smarter.sh/llm-client/stackademy-sql/release-date: 2024-06-15
    - smarter.sh/llm-client/stackademy-sql/backward-compatibility: true
    - smarter.sh/llm-client/stackademy-sql/note: |
        Uses SQL plugin to answer course catalogue questions.
        Very fast and accurate when the database is well-structured.
        Leverages cache for improved performance.
spec:
  config:
    prompt:
      deployed: false
      provider: openai
      defaultModel: gpt-6-luna
      defaultSystemRole: >
        You are a helpful assistant. When given the opportunity to utilize
        function calling, you should always do so. This will allow you to
        provide the best possible responses to the user. DO NOT GUESS. IF
          YOU DON'T KNOW THE ANSWER, RESPOND THAT YOU DON'T KNOW.
      defaultTemperature: 1.0
      defaultMaxTokens: 1024
      appName: Stackademy
      appAssistant: Stanley
      appWelcomeMessage: Welcome to Stackademy! How can I help you today?
      appExamplePrompts:
        - "Do you offer any courses on AI?"
        - "My budget is $1,000. What courses can I take?"
        - "I want to study programming. What do you suggest?"
      appPlaceholder: "Ask me anything about Stackademy courses..."
      appInfoUrl: https://stackademy.edu/online-courses
    styles:  # Stackademy's colors
      # appLogoUrl: https://cdn.smarter.sh/images/llmclients/stackademy/logo.png  # coming soon: upload, then uncomment
      # appBackgroundImageUrl: https://cdn.smarter.sh/images/llmclients/stackademy/background.png  # coming soon: upload, then uncomment
      # assistantAvatarUrl: https://cdn.smarter.sh/images/llmclients/stackademy/assistant.png  # coming soon: upload, then uncomment
      googleFont: Nunito
      fontSize: 15px
      borderRadius: 16px
      conversationHeader:
        backgroundColor: '#14213d'
        color: '#ffffff'
        infoColor: '#fca311'
      messageList:
        backgroundColor: rgba(255, 255, 255, 0.85)
        color: '#14213d'
      incomingMessage:
        backgroundColor: '#e5e5e5'
        color: '#14213d'
        linkColor: '#0b57d0'
      outgoingMessage:
        backgroundColor: '#fca311'
        color: '#14213d'
      messageInput:
        backgroundColor: '#ffffff'
        editorBackgroundColor: '#f1f1f1'
        placeholderColor: '#6c757d'
      buttons:
        color: '#fca311'
      typingIndicator:
        color: '#fca311'
  plugins:
    - stackademy_sql
  functions: []

More Examples

Every built-in LLMClient has styles, from a single color to a full theme, so each is an example to copy. They’re in the Smarter repository, under smarter/smarter/apps/llmclient/data/llm-clients/:

  • llmclient-simple.yaml: just a logo and one color. A little goes a long way.

  • llmclient-mcp-builder.yaml: a dark theme, like a code editor’s, in a monospace font.

  • llmclient-math.yaml: a chalkboard, with a handwriting font and a wooden frame.

  • llmclient-weather.yaml: a sunny sky, with round, puffy bubbles.

  • llmclient-research-analyst.yaml: a library’s reading room, in a serif font.

  • llmclient-smarter.yaml: Smarter’s own orange and gold.

Troubleshooting

Apply says a value “is not valid”. The error names the setting and shows the value. Check its form against A Two-Minute CSS Crash Course: a color needs its #, a size needs its unit (16px, not 16), and an image url must start with https:// (not http://).

Apply says an image “is not available”. Smarter couldn’t open the image. Paste the url into a private browser window: if you don’t see the image, nobody else will either. HTTP 403 or HTTP 404 usually means a typo, or a file that isn’t uploaded or isn’t public yet.

Apply says an image is “a text/html page, not an image”. The url is of a page that shows the image, such as a sharing link. Right-click the image, choose Copy Image Address, and use that url.

Apply says the manifest “has keys that it does not define”. A setting’s name is misspelled, or is in the wrong section. Names are case-sensitive: backgroundColor, not backgroundcolor or background_color.

Apply says that spec.config “is split into prompt … and styles”. The manifest was written before spec.config had its prompt and styles sections. Indent its settings under prompt:, and move appLogoUrl and appBackgroundImageUrl to styles:, as the error lists.

A hex color disappeared. In YAML, # starts a comment, so backgroundColor: #fca311 is an empty setting. Quote it: backgroundColor: "#fca311".

The font didn’t change. Check the name’s spelling against Google Fonts, including its capitals and spaces (Source Sans 3). If you also set fontFamily, it wins.

The background image doesn’t show. messageList.backgroundColor may be solid. Give it an opacity below 1, such as rgba(255, 255, 255, 0.85).

The logo or avatar doesn’t show. The image was available when you applied the manifest, but isn’t now. Paste its url into a browser to check it.

Under the Hood

This section is for the curious, and for web developers.

Smarter Chat is built on chatscope’s open source chat components, @chatscope/chat-ui-kit-react, and their theme, @chatscope/chat-ui-kit-styles. styles follows the theme’s design closely: its sections are chatscope’s components (MainContainer, ConversationHeader, MessageList, Message, MessageInput, Button, TypingIndicator and Avatar), and each setting is one of the theme’s variables, such as $conversation-header-bg-color for conversationHeader.backgroundColor. Each setting’s description in the Pydantic models names its variable.

chatscope’s theme is compiled ahead of time, so Smarter Chat can’t recompile it for each LLMClient. Instead, the config api returns the styles (with snake_case keys, as llmclient.styles), and the chat sets each one as a CSS custom property on its root element, .SmarterChat. Its stylesheet applies the properties to chatscope’s elements, with chatscope’s own value as each one’s fallback:

.SmarterChat .cs-conversation-header {
  background-color: var(--smarter-chat-header-bg, #f6fbff);
}

The properties are named --smarter-chat-<part>-<setting>, for example --smarter-chat-header-bg, --smarter-chat-incoming-link-color and --smarter-chat-input-placeholder-color, plus --smarter-chat-font-family, --smarter-chat-font-size and --smarter-chat-border-radius. A page that embeds the chat can set them itself, on .SmarterChat in its own stylesheet, and anything beyond them can be styled with chatscope’s cs- classes, and Smarter Chat’s smarter-chat- classes.

Because styles are written into every page that embeds the LLMClient, Smarter accepts only plain values: colors, sizes and font names that can’t contain anything else, and https image urls. Apply rejects any other value, and the chat ignores one that it receives. Apply also requests each image url, with the same protections as plugins’ web requests (public addresses only, validated redirects, timeouts), and requires HTTP 200 and a Content-Type that isn’t a web page.