@@ -507,4 +316,4 @@ export default function AIChatPanel({
);
-}
+}
\ No newline at end of file
diff --git a/modules/playground/components/chat-message.tsx b/modules/playground/components/chat-message.tsx
new file mode 100644
index 00000000..95b2bd06
--- /dev/null
+++ b/modules/playground/components/chat-message.tsx
@@ -0,0 +1,108 @@
+/**
+ * @fileoverview Chat message rendering component for AI conversations.
+ * @module components/chat-message
+ * @description Renders individual chat messages with support for:
+ * - User messages with avatar
+ * - AI assistant messages with bot avatar
+ * - Tool invocation display with status indicators
+ * - Loading states for streaming responses
+ * - Message parts (text, tool-invocation) from AI SDK v3
+ * @param {ChatMessageProps} props - Message data and loading state
+ * @returns {JSX.Element} Rendered message bubble with appropriate styling
+ */
+
+"use client";
+
+import React from "react";
+import { Bot, User, Wrench, Loader2 } from "lucide-react";
+
+interface MessagePart {
+ type?: string;
+ text?: string;
+ toolCallId?: string;
+ toolName?: string;
+ state?: string;
+ input?: Record
;
+ args?: Record;
+ [key: string]: unknown;
+}
+
+interface ExtendedMessage {
+ parts?: MessagePart[];
+ content?: string;
+ role?: string;
+ id?: string;
+}
+
+interface ChatMessageProps {
+ message: ExtendedMessage & { role?: string; id?: string };
+ isLoading?: boolean;
+}
+
+export function ChatMessage({ message, isLoading }: ChatMessageProps) {
+ const rawParts: MessagePart[] = message.parts ?? [];
+
+ // AI SDK v3 stores user text in parts[].type=="text"
+ const textParts = rawParts.filter((p) => (p.type ?? "") === "text");
+ const textContent: string = (
+ textParts.map((p) => p.text ?? "").join("") ||
+ message.content ||
+ ""
+ );
+
+ // v3 tool parts have type starting with "tool-" (e.g. "tool-read_file")
+ const toolParts: MessagePart[] = rawParts.filter(
+ (p) => (p.type ?? "").startsWith("tool-")
+ );
+
+ // Skip SDK-injected synthetic messages (no real text parts, no tool parts)
+ const isGenuineUser = message.role === "user" && textParts.length > 0;
+
+ return (
+
+ {isGenuineUser && (
+
+
+ {textContent}
+
+
+
+
+
+ )}
+ {message.role === "assistant" && (
+
+
+
+
+
+ {textContent && (
+
+ {textContent}
+
+ )}
+ {toolParts.map((ti, idx) => {
+ const tiName = (ti.toolName as string | undefined) ??
+ (ti.type as string)?.split("-").slice(1).join("-") ?? "tool";
+ const tiPath = (ti.input as Record
| undefined)?.path as string | undefined;
+ const tiDone = ti.state === "output-available" || ti.state === "result";
+ // Add index fallback for undefined toolCallId to fix React key issue
+ const key = ti.toolCallId ?? `tool-${idx}`;
+ return (
+
+
+
+
+
+ {tiName}({tiPath ? tiPath.split("/").pop() : ""}) {tiDone ? "✓" : }
+
+
+ );
+ })}
+
+
+ )}
+ {/* REMOVED: Duplicate "Thinking..." indicator - AIChatPanel handles this at list level */}
+
+ );
+}
\ No newline at end of file
diff --git a/modules/playground/components/env-manager.tsx b/modules/playground/components/env-manager.tsx
index bc41e6cd..48b5e1a0 100644
--- a/modules/playground/components/env-manager.tsx
+++ b/modules/playground/components/env-manager.tsx
@@ -154,7 +154,7 @@ export function EnvManager({
Environment Variables
-