Skip to content

Commit ed31a58

Browse files
committed
Resolves #378
1 parent c3e0b0c commit ed31a58

5 files changed

Lines changed: 70 additions & 30 deletions

File tree

.changeset/funny-bikes-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ai-elements": patch
3+
---
4+
5+
Update ConversationMessage to UIMessage

apps/docs/content/components/(chatbot)/conversation.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ import { ConversationDownload } from "@/components/ai-elements/conversation";
250250
type={{
251251
messages: {
252252
description: "Array of messages to include in the download.",
253-
type: "ConversationMessage[]",
253+
type: "UIMessage[]",
254254
required: true,
255255
},
256256
filename: {
@@ -260,7 +260,7 @@ import { ConversationDownload } from "@/components/ai-elements/conversation";
260260
},
261261
formatMessage: {
262262
description: "Custom function to format each message in the output.",
263-
type: "(message: ConversationMessage, index: number) => string",
263+
type: "(message: UIMessage, index: number) => string",
264264
},
265265
"...props": {
266266
description:
@@ -282,6 +282,6 @@ const markdown = messagesToMarkdown(messages);
282282
// With custom formatter
283283
const customMarkdown = messagesToMarkdown(
284284
messages,
285-
(msg, i) => `[${msg.role}]: ${msg.content}`
285+
(msg, i) => `[${msg.role}]: ${msg.parts.filter(p => p.type === "text").map(p => p.text).join("")}`
286286
);
287287
```

packages/elements/__tests__/conversation.test.tsx

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,16 @@ vi.mock<typeof import("use-stick-to-bottom")>(
6666
);
6767

6868
// Custom format function for messagesToMarkdown test
69-
const customFormatMessage = (msg: { role: string; content: string }) =>
70-
`[${msg.role}]: ${msg.content}`;
69+
const customFormatMessage = (msg: {
70+
role: string;
71+
parts: { type: string; text?: string }[];
72+
}) => {
73+
const text = msg.parts
74+
.filter((p) => p.type === "text")
75+
.map((p) => p.text)
76+
.join("");
77+
return `[${msg.role}]: ${text}`;
78+
};
7179

7280
describe("conversation", () => {
7381
it("renders children", () => {
@@ -219,13 +227,19 @@ describe("conversationScrollButton", () => {
219227
});
220228
});
221229

230+
const makeMessage = (role: "user" | "assistant" | "system", text: string) => ({
231+
id: `${role}-${text}`,
232+
parts: [{ text, type: "text" as const }],
233+
role,
234+
});
235+
222236
// Function name as describe title is a valid testing pattern
223237
// oxlint-disable-next-line eslint-plugin-jest(valid-title)
224238
describe(messagesToMarkdown, () => {
225239
it("converts messages to markdown format", () => {
226240
const messages = [
227-
{ content: "Hello", role: "user" as const },
228-
{ content: "Hi there!", role: "assistant" as const },
241+
makeMessage("user", "Hello"),
242+
makeMessage("assistant", "Hi there!"),
229243
];
230244

231245
const result = messagesToMarkdown(messages);
@@ -240,8 +254,8 @@ describe(messagesToMarkdown, () => {
240254

241255
it("uses custom formatMessage function", () => {
242256
const messages = [
243-
{ content: "Hello", role: "user" as const },
244-
{ content: "Hi", role: "assistant" as const },
257+
makeMessage("user", "Hello"),
258+
makeMessage("assistant", "Hi"),
245259
];
246260

247261
const result = messagesToMarkdown(messages, customFormatMessage);
@@ -251,20 +265,39 @@ describe(messagesToMarkdown, () => {
251265

252266
it("handles all role types", () => {
253267
const messages = [
254-
{ content: "User msg", role: "user" as const },
255-
{ content: "Assistant msg", role: "assistant" as const },
256-
{ content: "System msg", role: "system" as const },
257-
{ content: "Tool msg", role: "tool" as const },
258-
{ content: "Data msg", role: "data" as const },
268+
makeMessage("user", "User msg"),
269+
makeMessage("assistant", "Assistant msg"),
270+
makeMessage("system", "System msg"),
259271
];
260272

261273
const result = messagesToMarkdown(messages);
262274

263275
expect(result).toContain("**User:** User msg");
264276
expect(result).toContain("**Assistant:** Assistant msg");
265277
expect(result).toContain("**System:** System msg");
266-
expect(result).toContain("**Tool:** Tool msg");
267-
expect(result).toContain("**Data:** Data msg");
278+
});
279+
280+
it("extracts text from multiple parts", () => {
281+
const message = {
282+
id: "multi",
283+
parts: [
284+
{ text: "Hello ", type: "text" as const },
285+
{
286+
args: {},
287+
result: {},
288+
state: "result" as const,
289+
toolInvocationId: "1",
290+
toolName: "test",
291+
type: "tool-invocation" as const,
292+
},
293+
{ text: "world", type: "text" as const },
294+
],
295+
role: "assistant" as const,
296+
};
297+
298+
const result = messagesToMarkdown([message]);
299+
300+
expect(result).toBe("**Assistant:** Hello world");
268301
});
269302
});
270303

@@ -310,8 +343,8 @@ const setupDomClickTracker = () => {
310343

311344
describe("conversationDownload", () => {
312345
const mockMessages = [
313-
{ content: "Hello", role: "user" as const },
314-
{ content: "Hi there!", role: "assistant" as const },
346+
makeMessage("user", "Hello"),
347+
makeMessage("assistant", "Hi there!"),
315348
];
316349

317350
it("renders download button", () => {

packages/elements/src/conversation.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Button } from "@repo/shadcn-ui/components/ui/button";
44
import { cn } from "@repo/shadcn-ui/lib/utils";
5+
import type { UIMessage } from "ai";
56
import { ArrowDownIcon, DownloadIcon } from "lucide-react";
67
import type { ComponentProps } from "react";
78
import { useCallback } from "react";
@@ -99,30 +100,31 @@ export const ConversationScrollButton = ({
99100
);
100101
};
101102

102-
export interface ConversationMessage {
103-
role: "user" | "assistant" | "system" | "data" | "tool";
104-
content: string;
105-
}
103+
const getMessageText = (message: UIMessage): string =>
104+
message.parts
105+
.filter((part) => part.type === "text")
106+
.map((part) => part.text)
107+
.join("");
106108

107109
export type ConversationDownloadProps = Omit<
108110
ComponentProps<typeof Button>,
109111
"onClick"
110112
> & {
111-
messages: ConversationMessage[];
113+
messages: UIMessage[];
112114
filename?: string;
113-
formatMessage?: (message: ConversationMessage, index: number) => string;
115+
formatMessage?: (message: UIMessage, index: number) => string;
114116
};
115117

116-
const defaultFormatMessage = (message: ConversationMessage): string => {
118+
const defaultFormatMessage = (message: UIMessage): string => {
117119
const roleLabel =
118120
message.role.charAt(0).toUpperCase() + message.role.slice(1);
119-
return `**${roleLabel}:** ${message.content}`;
121+
return `**${roleLabel}:** ${getMessageText(message)}`;
120122
};
121123

122124
export const messagesToMarkdown = (
123-
messages: ConversationMessage[],
125+
messages: UIMessage[],
124126
formatMessage: (
125-
message: ConversationMessage,
127+
message: UIMessage,
126128
index: number
127129
) => string = defaultFormatMessage
128130
): string => messages.map((msg, i) => formatMessage(msg, i)).join("\n\n");

skills/ai-elements/references/conversation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,9 @@ import { ConversationDownload } from "@/components/ai-elements/conversation";
198198

199199
| Prop | Type | Default | Description |
200200
|------|------|---------|-------------|
201-
| `messages` | `ConversationMessage[]` | Required | Array of messages to include in the download. |
201+
| `messages` | `UIMessage[]` | Required | Array of messages to include in the download. |
202202
| `filename` | `string` | - | The filename for the downloaded file. |
203-
| `formatMessage` | `(message: ConversationMessage, index: number) => string` | - | Custom function to format each message in the output. |
203+
| `formatMessage` | `(message: UIMessage, index: number) => string` | - | Custom function to format each message in the output. |
204204
| `...props` | `Omit<ComponentProps<typeof Button>, ` | - | Any other props are spread to the underlying shadcn/ui Button component. |
205205

206206
### `messagesToMarkdown`

0 commit comments

Comments
 (0)