# Message

One turn of a conversation: the person's message, and the agent's reply with actions to copy, rate and retry it.

Source: https://suiss.app/ui/docs/ai/message

**React**

**Example**

```tsx
"use client"

import {
  ArrowsClockwiseIcon,
  CheckIcon,
  CopyIcon,
  PencilSimpleIcon,
  ThumbsDownIcon,
  ThumbsUpIcon,
} from "@phosphor-icons/react"
import * as React from "react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Message, MessageContent, MessageFooter } from "@/components/ui/message"
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip"

const command = "npx @esuiss/ui@latest add dialog"

const reply = `Install Dialog, then open it from the gear button in site-header.tsx.

${command}

Wrap the button in a DialogTrigger and put the settings form in DialogContent. Focus moves into the dialog when it opens, and Esc closes it.`

function MessageAction({
  label,
  children,
  ...props
}: React.ComponentProps<typeof Button> & { label: string }) {
  return (
    <Tooltip>
      <TooltipTrigger
        render={
          <Button
            variant="ghost"
            size="icon-sm"
            aria-label={label}
            {...props}
          />
        }
      >
        {children}
      </TooltipTrigger>
      <TooltipContent>{label}</TooltipContent>
    </Tooltip>
  )
}

export default function MessagePattern() {
  const [copied, setCopied] = React.useState(false)
  const [feedback, setFeedback] = React.useState<"like" | "dislike" | null>(
    null
  )

  React.useEffect(() => {
    if (!copied) {
      return
    }

    const timeout = setTimeout(() => setCopied(false), 2000)
    return () => clearTimeout(timeout)
  }, [copied])

  return (
    <div className="flex w-full max-w-md flex-col gap-8 py-6 text-sm">
      <Message align="end">
        <MessageContent className="gap-2">
          <Bubble align="end" variant="muted">
            <BubbleContent>
              How do I add a settings dialog? It should open from the gear icon
              in the header.
            </BubbleContent>
          </Bubble>
          <MessageFooter>
            <MessageAction label="Copy">
              <CopyIcon />
            </MessageAction>
            <MessageAction label="Edit">
              <PencilSimpleIcon />
            </MessageAction>
          </MessageFooter>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-3 text-sm [&_code]:font-mono [&_code]:text-[0.9em]">
          <p>
            Install Dialog, then open it from the gear button in{" "}
            <code>site-header.tsx</code>.
          </p>
          <CodeBlock size="sm">
            <CodeBlockContent>{command}</CodeBlockContent>
          </CodeBlock>
          <p>
            Wrap the button in a <code>DialogTrigger</code> and put the settings
            form in <code>DialogContent</code>. Focus moves into the dialog when
            it opens, and Esc closes it.
          </p>
          <MessageFooter className="px-0">
            <MessageAction
              label={copied ? "Copied" : "Copy"}
              onClick={() => {
                navigator.clipboard?.writeText(reply).catch(() => undefined)
                setCopied(true)
              }}
            >
              {copied ? (
                <CheckIcon />
              ) : (
                <CopyIcon />
              )}
            </MessageAction>
            <MessageAction
              label="Good response"
              aria-pressed={feedback === "like"}
              onClick={() => setFeedback(feedback === "like" ? null : "like")}
            >
              <ThumbsUpIcon
                className={feedback === "like" ? "fill-current" : undefined}
              />
            </MessageAction>
            <MessageAction
              label="Bad response"
              aria-pressed={feedback === "dislike"}
              onClick={() =>
                setFeedback(feedback === "dislike" ? null : "dislike")
              }
            >
              <ThumbsDownIcon
                className={feedback === "dislike" ? "fill-current" : undefined}
              />
            </MessageAction>
            <MessageAction label="Retry">
              <ArrowsClockwiseIcon />
            </MessageAction>
          </MessageFooter>
        </MessageContent>
      </Message>
    </div>
  )
}
```

Use this pattern for one turn in a thread. The person's text is a [Bubble](https://suiss.app/ui/docs/components/bubble) on a [Message](https://suiss.app/ui/docs/components/message) with `align="end"`. The agent's reply is plain content on a Message, with actions under it. A thread of these turns is a [Chat](https://suiss.app/ui/docs/ai/chat).

## Installation

The pattern is built with Message, Bubble, Button and Tooltip.

```bash title="Terminal"
npx shadcn@latest add @esuiss/message @esuiss/bubble @esuiss/button @esuiss/tooltip
```

Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add message bubble button tooltip
```

## Usage

```tsx
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import { Message, MessageContent, MessageFooter } from "@/components/ui/message"
```

```tsx
<Message align="end">
  <MessageContent>
    <Bubble align="end" variant="muted">
      <BubbleContent>How do I add a settings dialog?</BubbleContent>
    </Bubble>
  </MessageContent>
</Message>
<Message>
  <MessageContent className="gap-2">
    <p>Install Dialog, then open it from the gear button.</p>
    <MessageFooter>
      <Button variant="ghost" size="icon-sm" aria-label="Copy">
        <CopyIcon />
      </Button>
    </MessageFooter>
  </MessageContent>
</Message>
```

Put actions in `MessageFooter`. It follows the message alignment, so on the person's turn the actions sit at the end. Use ghost icon buttons with a tooltip for each:

- **Copy** writes the reply to the clipboard and swaps to a check for two seconds.
- **Good response** and **Bad response** are toggles. Set `aria-pressed` so screen readers announce the state.
- **Retry** runs the turn again.
- **Edit** on the person's turn puts the text back in the field.

Render markdown replies with your markdown renderer of choice, and route fenced code to a [Code Block](https://suiss.app/ui/docs/components/code-block).

## AI SDK

Branch on the message role. The person's turn is the bubble. The agent's turn is the reply, and `regenerate` with the message id runs it again.

```tsx
const { messages, regenerate } = useChat()

if (message.role === "user") {
  return (
    <Message align="end">
      <MessageContent>
        <Bubble align="end" variant="muted">
          <BubbleContent>{text}</BubbleContent>
        </Bubble>
      </MessageContent>
    </Message>
  )
}

return (
  <Message>
    <MessageContent className="gap-2">
      <Markdown>{text}</Markdown>
      <MessageFooter>
        <Button
          variant="ghost"
          size="icon-sm"
          aria-label="Retry"
          onClick={() => regenerate({ messageId: message.id })}
        >
          <RefreshCcwIcon />
        </Button>
      </MessageFooter>
    </MessageContent>
  </Message>
)
```

## API Reference

See [Message](https://suiss.app/ui/docs/components/message#api-reference), [Bubble](https://suiss.app/ui/docs/components/bubble#api-reference) and [Tooltip](https://suiss.app/ui/docs/components/tooltip#api-reference).

## Accessibility

- Every action is an icon button, so give each an `aria-label`; the tooltip repeats it for pointer users.
- Change the label when the state changes, such as "Copy" to "Copied", so the change is announced.
- Feedback buttons are toggles and set `aria-pressed`.

**React Native, SwiftUI, Jetpack Compose**

```tsx
import { ArrowClockwiseIcon, ThumbsDownIcon, ThumbsUpIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import { CodeBlock, CodeBlockActions, CodeBlockContent, CodeBlockCopyButton, CodeBlockHeader, CodeBlockTitle } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { Message, MessageContent } from "@/components/ui/message"
import { Text } from "@/components/ui/text"

const command = "npx @esuiss/ui@latest add dialog"

export function AiMessageExample() {
  return (
    <View className="w-full gap-4">
      <Message align="end">
        <MessageContent>
          <Bubble variant="muted">
            <BubbleContent>How do I add a dialog?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <View className="gap-3">
        <Text>Add it with the CLI, then import it where you need it.</Text>
        <CodeBlock size="sm">
          <CodeBlockHeader>
            <CodeBlockTitle>Terminal</CodeBlockTitle>
            <CodeBlockActions>
              <CodeBlockCopyButton value={command} />
            </CodeBlockActions>
          </CodeBlockHeader>
          <CodeBlockContent language="bash">{command}</CodeBlockContent>
        </CodeBlock>
        <View className="flex-row gap-1">
          <Button
            variant="ghost"
            size="icon-sm"
            accessibilityLabel="Good reply"
          >
            <Icon as={ThumbsUpIcon} />
          </Button>
          <Button variant="ghost" size="icon-sm" accessibilityLabel="Bad reply">
            <Icon as={ThumbsDownIcon} />
          </Button>
          <Button variant="ghost" size="icon-sm" accessibilityLabel="Retry">
            <Icon as={ArrowClockwiseIcon} />
          </Button>
        </View>
      </View>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct MessageExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      Message(align: .end) {
        MessageContent {
          Bubble(variant: .muted, align: .end) { BubbleContent("How do I add a dialog?") }
        }
      }
      VStack(alignment: .leading, spacing: 12) {
        Text("Add it with the CLI, then import it where you need it.").textStyle(.base).foregroundStyle(EsuissColor.foreground)
        CodeBlock(size: .sm) {
          CodeBlockHeader {
            CodeBlockTitle("Terminal")
            CodeBlockActions { CodeBlockCopyButton(value: "npx @esuiss/ui@latest add dialog") }
          }
          CodeBlockContent("npx @esuiss/ui@latest add dialog", language: "bash")
        }
        HStack(spacing: 4) {
          AIIconButton(label: "Good reply", image: Ph.thumbsUp.regular)
          AIIconButton(label: "Bad reply", image: Ph.thumbsDown.regular)
          AIIconButton(label: "Retry", image: Ph.arrowClockwise.regular)
        }
      }
    }
  }
}

struct AIIconButton: View {
  let label: String
  let image: Image

  var body: some View {
    Button {} label: {
      image.frame(width: 16, height: 16)
    }
    .buttonStyle(.variant(.ghost, size: .iconSm))
    .accessibilityLabel(Text(label))
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.ArrowClockwise
import com.adamglin.phosphoricons.regular.ThumbsDown
import com.adamglin.phosphoricons.regular.ThumbsUp
import com.esuiss.ui.components.Bubble
import com.esuiss.ui.components.BubbleContent
import com.esuiss.ui.components.BubbleVariant
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockActions
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockCopyButton
import com.esuiss.ui.components.CodeBlockHeader
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.CodeBlockTitle
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Message
import com.esuiss.ui.components.MessageAlign
import com.esuiss.ui.components.MessageContent
import com.esuiss.ui.components.Text

@Composable
fun MessageAIDefault() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Message(Modifier.fillMaxWidth(), align = MessageAlign.End) {
            MessageContent {
                Bubble(variant = BubbleVariant.Muted) { BubbleContent("How do I add a dialog?") }
            }
        }
        Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
            Text("Add it with the CLI, then import it where you need it.")
            CodeBlock(size = CodeBlockSize.Sm) {
                CodeBlockHeader {
                    CodeBlockTitle("Terminal")
                    CodeBlockActions { CodeBlockCopyButton("npx @esuiss/ui@latest add dialog") }
                }
                CodeBlockContent("npx @esuiss/ui@latest add dialog", language = "bash")
            }
            Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                Button(onClick = {}, variant = ButtonVariant.Ghost, size = ButtonSize.IconSm) {
                    Icon(PhosphorIcons.Regular.ThumbsUp, contentDescription = "Good reply")
                }
                Button(onClick = {}, variant = ButtonVariant.Ghost, size = ButtonSize.IconSm) {
                    Icon(PhosphorIcons.Regular.ThumbsDown, contentDescription = "Bad reply")
                }
                Button(onClick = {}, variant = ButtonVariant.Ghost, size = ButtonSize.IconSm) {
                    Icon(PhosphorIcons.Regular.ArrowClockwise, contentDescription = "Retry")
                }
            }
        }
    }
}
```

Message on mobile is built from the same parts as on the web: [Message](https://suiss.app/ui/docs/components/message), [Bubble](https://suiss.app/ui/docs/components/bubble), [Code Block](https://suiss.app/ui/docs/components/code-block), [Button](https://suiss.app/ui/docs/components/button). The example above is a static demo; connect it to your agent by rendering the same parts from your messages.

## Installation

**React Native**

```bash title="Terminal"
npx @esuiss/ui@latest add message bubble code-block button
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add message bubble code-block button --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add message bubble code-block button --platform kotlin
```

## API Reference

See [Message](https://suiss.app/ui/docs/components/message), [Bubble](https://suiss.app/ui/docs/components/bubble), [Code Block](https://suiss.app/ui/docs/components/code-block), [Button](https://suiss.app/ui/docs/components/button).

