Skip to content

Commit 3cc2e71

Browse files
committed
fix(docs-ai): stabilize tool-first chat flow and message interactions
1 parent 411cdd8 commit 3cc2e71

File tree

2 files changed

+200
-45
lines changed

2 files changed

+200
-45
lines changed

docs/app/api/chat/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export async function POST(req: Request) {
5858
system: systemPrompt,
5959
messages,
6060
tools,
61-
stopWhen: stepCountIs(5),
61+
stopWhen: stepCountIs(8),
6262
});
6363

6464
return result.toUIMessageStreamResponse();

docs/components/ai-panel/chat-message.tsx

Lines changed: 199 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface ToolRenderContext {
2424
hasCodeTool: boolean;
2525
hasComponentExample: boolean;
2626
hasInstallation: boolean;
27+
hasReactTypeTable: boolean;
2728
hasRelatedLinks: boolean;
2829
relatedLinkUrls: string[];
2930
}
@@ -52,6 +53,50 @@ function getRelatedLinksFromOutput(output: unknown): Array<{ title: string; url:
5253
.filter((link): link is { title: string; url: string } => link !== null);
5354
}
5455

56+
function getReactTypeTableRowsFromOutput(output: unknown): Array<{
57+
name: string;
58+
type: string;
59+
required: boolean;
60+
description: string;
61+
defaultValue: string | null;
62+
}> {
63+
const safeOutput = getRecord(output);
64+
const rows = safeOutput.rows;
65+
if (!Array.isArray(rows)) return [];
66+
67+
return rows
68+
.map((row) => {
69+
const safeRow = getRecord(row);
70+
71+
if (
72+
typeof safeRow.name !== "string" ||
73+
typeof safeRow.type !== "string" ||
74+
typeof safeRow.required !== "boolean"
75+
) {
76+
return null;
77+
}
78+
79+
return {
80+
name: safeRow.name,
81+
type: safeRow.type,
82+
required: safeRow.required,
83+
description: typeof safeRow.description === "string" ? safeRow.description : "",
84+
defaultValue: typeof safeRow.defaultValue === "string" ? safeRow.defaultValue : null,
85+
};
86+
})
87+
.filter(
88+
(
89+
row,
90+
): row is {
91+
name: string;
92+
type: string;
93+
required: boolean;
94+
description: string;
95+
defaultValue: string | null;
96+
} => row !== null,
97+
);
98+
}
99+
55100
function getToolCopyText(toolName: string, input: unknown, output: unknown): string[] {
56101
const lines: string[] = [];
57102
const safeInput = getRecord(input);
@@ -88,6 +133,21 @@ function getToolCopyText(toolName: string, input: unknown, output: unknown): str
88133
}
89134
}
90135

136+
if (toolName === "showReactTypeTable") {
137+
const rows = getReactTypeTableRowsFromOutput(output);
138+
if (rows.length > 0) {
139+
lines.push("## Props");
140+
lines.push(
141+
...rows.map((row) => {
142+
const requiredText = row.required ? " (required)" : "";
143+
const defaultText = row.defaultValue ? ` (default: ${row.defaultValue})` : "";
144+
const descriptionText = row.description ? ` - ${row.description}` : "";
145+
return `- ${row.name}${requiredText}: ${row.type}${defaultText}${descriptionText}`;
146+
}),
147+
);
148+
}
149+
}
150+
91151
if (toolName === "findRelatedLinks") {
92152
const links = getRelatedLinksFromOutput(output);
93153
if (links.length > 0) {
@@ -134,6 +194,7 @@ function getToolRenderContext(message: UIMessage): ToolRenderContext {
134194
hasCodeTool: false,
135195
hasComponentExample: false,
136196
hasInstallation: false,
197+
hasReactTypeTable: false,
137198
hasRelatedLinks: false,
138199
relatedLinkUrls: [],
139200
};
@@ -143,6 +204,7 @@ function getToolRenderContext(message: UIMessage): ToolRenderContext {
143204
if (part.toolName === "showCodeBlock") context.hasCodeTool = true;
144205
if (part.toolName === "showComponentExample") context.hasComponentExample = true;
145206
if (part.toolName === "showInstallation") context.hasInstallation = true;
207+
if (part.toolName === "showReactTypeTable") context.hasReactTypeTable = true;
146208
if (part.toolName === "findRelatedLinks") {
147209
context.hasRelatedLinks = true;
148210
context.relatedLinkUrls.push(
@@ -161,6 +223,7 @@ function getToolRenderContext(message: UIMessage): ToolRenderContext {
161223
if (toolName === "showCodeBlock") context.hasCodeTool = true;
162224
if (toolName === "showComponentExample") context.hasComponentExample = true;
163225
if (toolName === "showInstallation") context.hasInstallation = true;
226+
if (toolName === "showReactTypeTable") context.hasReactTypeTable = true;
164227
if (toolName === "findRelatedLinks") {
165228
context.hasRelatedLinks = true;
166229
context.relatedLinkUrls.push(
@@ -174,63 +237,149 @@ function getToolRenderContext(message: UIMessage): ToolRenderContext {
174237
return context;
175238
}
176239

177-
function isRedundantTextForTools(text: string, toolContext: ToolRenderContext): boolean {
178-
const trimmed = text.trim();
179-
if (!trimmed) return true;
240+
function isCoveredRelatedUrl(urlInText: string, relatedLinkUrls: string[]): boolean {
241+
return relatedLinkUrls.some(
242+
(toolUrl) => toolUrl.includes(urlInText) || urlInText.includes(toolUrl),
243+
);
244+
}
245+
246+
function sanitizeTextForTools(text: string, toolContext: ToolRenderContext): string {
247+
let sanitized = text;
248+
249+
if (!sanitized.trim()) {
250+
return "";
251+
}
180252

181253
if (
182-
(toolContext.hasCodeTool || toolContext.hasComponentExample || toolContext.hasInstallation) &&
183-
/```/.test(trimmed)
254+
(toolContext.hasCodeTool ||
255+
toolContext.hasComponentExample ||
256+
toolContext.hasInstallation ||
257+
toolContext.hasReactTypeTable) &&
258+
/```/.test(sanitized)
184259
) {
185-
return true;
260+
return "";
186261
}
187262

188263
if (toolContext.hasInstallation) {
189-
if (/@seed-design\/cli@latest add/.test(trimmed)) {
190-
return true;
264+
sanitized = sanitized
265+
.replace(/^#{1,6}\s*(installation|install|)\s*$/gim, "")
266+
.replace(/^.*@seed-design\/cli@latest add.*$/gim, "")
267+
.replace(/^.*(run this command|to install|.*| ).*$/gim, "");
268+
}
269+
270+
if (toolContext.hasComponentExample) {
271+
sanitized = sanitized
272+
.replace(/^#{1,6}\s*(preview||example| )\s*$/gim, "")
273+
.replace(
274+
/^.*(here is a preview|preview of the|component preview| | | .*).*$/gim,
275+
"",
276+
);
277+
}
278+
279+
if (toolContext.hasReactTypeTable) {
280+
const propsListPatterns = [
281+
/^#{1,6}\s*props\s*$/gim,
282+
/^.*(\s*props|props |prop table|props table| | ).*$/gim,
283+
/^\s*[-*]\s*\*\*[^*]+\*\*.*$/gim,
284+
];
285+
286+
for (const pattern of propsListPatterns) {
287+
sanitized = sanitized.replace(pattern, "");
191288
}
192-
if (
193-
trimmed.length < 220 &&
194-
/(run this command|to install|.*| )/i.test(trimmed)
195-
) {
196-
return true;
289+
290+
if (/\b(props?||)\b/i.test(sanitized) && /\|\s*undefined/.test(sanitized)) {
291+
return "";
197292
}
198293
}
199294

200-
if (toolContext.hasComponentExample) {
201-
if (
202-
trimmed.length < 220 &&
203-
/(here is a preview|preview of the|component preview| | | .*)/i.test(
204-
trimmed,
205-
)
206-
) {
207-
return true;
295+
if (toolContext.hasRelatedLinks) {
296+
sanitized = sanitized
297+
.replace(/^#{1,6}\s*(related links?| | )\s*$/gim, "")
298+
.replace(/^.*(for more detailed information| ).*$/gim, "");
299+
300+
const sanitizedLines = sanitized
301+
.split("\n")
302+
.filter((line) => {
303+
const trimmedLine = line.trim();
304+
if (!trimmedLine) return true;
305+
306+
const markdownLinkMatch = trimmedLine.match(/\[.*?\]\((https?:\/\/[^)]+)\)/);
307+
if (markdownLinkMatch) {
308+
return !isCoveredRelatedUrl(markdownLinkMatch[1], toolContext.relatedLinkUrls);
309+
}
310+
311+
const urlMatches = trimmedLine.match(/https?:\/\/[^\s)\]]+/g) ?? [];
312+
if (urlMatches.length === 0) return true;
313+
314+
return !urlMatches.every((urlInText) =>
315+
isCoveredRelatedUrl(urlInText, toolContext.relatedLinkUrls),
316+
);
317+
})
318+
.join("\n");
319+
320+
if (sanitizedLines.trim().length === 0) {
321+
return "";
208322
}
323+
324+
sanitized = sanitizedLines;
325+
}
326+
327+
sanitized = sanitized
328+
.split("\n")
329+
.map((line) => line.replace(/\s+$/g, ""))
330+
.join("\n")
331+
.replace(/\n{3,}/g, "\n\n")
332+
.trim();
333+
334+
if (!sanitized) {
335+
return "";
336+
}
337+
338+
if (
339+
toolContext.hasInstallation &&
340+
sanitized.length < 220 &&
341+
/(run this command|to install|.*| )/i.test(sanitized)
342+
) {
343+
return "";
344+
}
345+
346+
if (
347+
toolContext.hasComponentExample &&
348+
sanitized.length < 220 &&
349+
/(here is a preview|preview of the|component preview| | | .*)/i.test(
350+
sanitized,
351+
)
352+
) {
353+
return "";
354+
}
355+
356+
if (
357+
toolContext.hasRelatedLinks &&
358+
/(related links?| | |for more detailed information| )/i.test(
359+
sanitized,
360+
)
361+
) {
362+
return "";
363+
}
364+
365+
if (
366+
toolContext.hasReactTypeTable &&
367+
/(\s*props|props |prop table|props table| )/i.test(sanitized)
368+
) {
369+
return "";
209370
}
210371

211372
if (toolContext.hasRelatedLinks) {
373+
const urlMatches = sanitized.match(/https?:\/\/[^\s)\]]+/g) ?? [];
212374
if (
213-
/(related links?| | |for more detailed information| )/i.test(
214-
trimmed,
215-
)
375+
urlMatches.length > 0 &&
376+
urlMatches.every((urlInText) => isCoveredRelatedUrl(urlInText, toolContext.relatedLinkUrls))
216377
) {
217-
return true;
218-
}
219-
220-
const urlMatches = trimmed.match(/https?:\/\/[^\s)\]]+/g) ?? [];
221-
if (urlMatches.length > 0) {
222-
const allCoveredByTool = urlMatches.every((urlInText) =>
223-
toolContext.relatedLinkUrls.some(
224-
(toolUrl) => toolUrl.includes(urlInText) || urlInText.includes(toolUrl),
225-
),
226-
);
227-
if (allCoveredByTool) {
228-
return true;
229-
}
378+
return "";
230379
}
231380
}
232381

233-
return false;
382+
return sanitized;
234383
}
235384

236385
export function ChatMessage({ message }: { message: UIMessage }) {
@@ -241,6 +390,7 @@ export function ChatMessage({ message }: { message: UIMessage }) {
241390
hasCodeTool: false,
242391
hasComponentExample: false,
243392
hasInstallation: false,
393+
hasReactTypeTable: false,
244394
hasRelatedLinks: false,
245395
relatedLinkUrls: [],
246396
}
@@ -272,7 +422,7 @@ export function ChatMessage({ message }: { message: UIMessage }) {
272422

273423
return (
274424
<div className={`flex ${isUser ? "justify-end" : "justify-start"}`}>
275-
<div className="max-w-[90%]">
425+
<div className={isUser ? "max-w-[90%]" : "min-w-[85%] max-w-[90%]"}>
276426
<div
277427
className={`${
278428
isUser
@@ -293,7 +443,8 @@ export function ChatMessage({ message }: { message: UIMessage }) {
293443
!isUser &&
294444
(toolContext.hasCodeTool ||
295445
toolContext.hasComponentExample ||
296-
toolContext.hasInstallation)
446+
toolContext.hasInstallation ||
447+
toolContext.hasReactTypeTable)
297448
) {
298449
return null;
299450
}
@@ -309,7 +460,11 @@ export function ChatMessage({ message }: { message: UIMessage }) {
309460
return null;
310461
}
311462

312-
if (!isUser && isRedundantTextForTools(segment.text, toolContext)) {
463+
const visibleText = !isUser
464+
? sanitizeTextForTools(segment.text, toolContext)
465+
: segment.text;
466+
467+
if (!visibleText) {
313468
return null;
314469
}
315470

@@ -322,7 +477,7 @@ export function ChatMessage({ message }: { message: UIMessage }) {
322477
: "prose prose-sm dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
323478
}`}
324479
>
325-
{segment.text}
480+
{visibleText}
326481
</div>
327482
);
328483
})}
@@ -388,7 +543,7 @@ export function ChatMessage({ message }: { message: UIMessage }) {
388543
initial={{ opacity: 0 }}
389544
animate={{ opacity: 1 }}
390545
exit={{ opacity: 0 }}
391-
transition={{ duration: 0.15 }}
546+
transition={{ duration: 0.1 }}
392547
className="inline-flex"
393548
>
394549
<Icon svg={<IconCheckmarkCircleLine />} />
@@ -399,7 +554,7 @@ export function ChatMessage({ message }: { message: UIMessage }) {
399554
initial={{ opacity: 0 }}
400555
animate={{ opacity: 1 }}
401556
exit={{ opacity: 0 }}
402-
transition={{ duration: 0.15 }}
557+
transition={{ duration: 0.1 }}
403558
className="inline-flex"
404559
>
405560
<Icon svg={<IconSquare2StackedLine />} />

0 commit comments

Comments
 (0)