From 1de6b9bfe1f2da245d5ef4bb83556457ffa7da57 Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Tue, 4 Nov 2025 06:08:28 -0800 Subject: [PATCH 1/9] update chat demos to use loremllm --- packages/examples/package.json | 2 + packages/examples/src/demo-chat-data.tsx | 194 +++++++ packages/examples/src/demo-chatgpt.tsx | 673 +++++------------------ packages/examples/src/demo-claude.tsx | 2 +- packages/examples/src/queue.tsx | 13 +- pnpm-lock.yaml | 108 +++- 6 files changed, 437 insertions(+), 555 deletions(-) create mode 100644 packages/examples/src/demo-chat-data.tsx diff --git a/packages/examples/package.json b/packages/examples/package.json index 76ef5fbe..8bec68b8 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -7,7 +7,9 @@ "./*": "./src/*.tsx" }, "dependencies": { + "@ai-sdk/react": "^2.0.87", "@icons-pack/react-simple-icons": "^13.8.0", + "@loremllm/transport": "^0.4.3", "@repo/elements": "workspace:*", "@xyflow/react": "^12.9.0", "ai": "5.1.0-beta.22", diff --git a/packages/examples/src/demo-chat-data.tsx b/packages/examples/src/demo-chat-data.tsx new file mode 100644 index 00000000..dc665d5b --- /dev/null +++ b/packages/examples/src/demo-chat-data.tsx @@ -0,0 +1,194 @@ +"use client"; + +import type { UIMessage } from "ai"; +import { + BarChartIcon, + BoxIcon, + CodeSquareIcon, + GraduationCapIcon, + NotepadTextIcon, +} from "lucide-react"; +import { nanoid } from "nanoid"; + +// Mock messages as Map of user message text -> assistant message parts +export const mockMessages = new Map([ + [ + "Can you explain how to use React hooks effectively?", + [ + { + type: "source-url", + sourceId: nanoid(), + url: "https://react.dev/reference/react", + title: "React Documentation", + }, + { + type: "source-url", + sourceId: nanoid(), + url: "https://react.dev/reference/react-dom", + title: "React DOM Documentation", + }, + // { + // type: "tool-mcp", + // toolCallId: nanoid(), + // state: "output-available" as const, + // input: { + // query: "React hooks best practices", + // source: "react.dev", + // }, + // output: `{ + // "query": "React hooks best practices", + // "results": [ + // { + // "title": "Rules of Hooks", + // "url": "https://react.dev/warnings/invalid-hook-call-warning", + // "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." + // }, + // { + // "title": "useState Hook", + // "url": "https://react.dev/reference/react/useState", + // "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." + // }, + // { + // "title": "useEffect Hook", + // "url": "https://react.dev/reference/react/useEffect", + // "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." + // } + // ] + // }`, + // }, + { + type: "text", + text: `# React Hooks Best Practices + +React hooks are a powerful feature that let you use state and other React features without writing classes. Here are some tips for using them effectively: + +## Rules of Hooks + +1. **Only call hooks at the top level** of your component or custom hooks +2. **Don't call hooks inside loops, conditions, or nested functions** + +## Common Hooks + +- **useState**: For local component state +- **useEffect**: For side effects like data fetching +- **useContext**: For consuming context +- **useReducer**: For complex state logic +- **useCallback**: For memoizing functions +- **useMemo**: For memoizing values + +## Example of useState and useEffect + +\`\`\`jsx +function ProfilePage({ userId }) { + const [user, setUser] = useState(null); + + useEffect(() => { + // This runs after render and when userId changes + fetchUser(userId).then(userData => { + setUser(userData); + }); + }, [userId]); + + return user ? : ; +} +\`\`\` + +Would you like me to explain any specific hook in more detail?`, + }, + ], + ], + [ + "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", + [ + { + type: "reasoning", + text: `The user is asking for a detailed explanation of useCallback and useMemo. I should provide a clear and concise explanation of each hook's purpose and how they differ. + +The useCallback hook is used to memoize functions to prevent unnecessary re-renders of child components that receive functions as props. + +The useMemo hook is used to memoize values to avoid expensive recalculations on every render. + +Both hooks help with performance optimization, but they serve different purposes.`, + }, + { + type: "text", + text: `## useCallback vs useMemo + +Both hooks help with *performance optimization*, but they serve different purposes: + +### useCallback + +\`useCallback\` memoizes **functions** to prevent unnecessary re-renders of child components that receive functions as props. + +\`\`\`jsx +// Without useCallback - a new function is created on every render +const handleClick = () => { + console.log(count); +}; + +// With useCallback - the function is only recreated when dependencies change +const handleClick = useCallback(() => { + console.log(count); +}, [count]); +\`\`\` + +### useMemo + +\`useMemo\` memoizes **values** to avoid expensive recalculations on every render. + +\`\`\`jsx +// Without useMemo - expensive calculation runs on every render +const sortedList = expensiveSort(items); + +// With useMemo - calculation only runs when items change +const sortedList = useMemo(() => expensiveSort(items), [items]); +\`\`\` + +### When to use which? + +- Use **useCallback** when: + - Passing callbacks to optimized child components that rely on reference equality + - Working with event handlers that you pass to child components + +- Use **useMemo** when: + - You have computationally expensive calculations + - You want to avoid recreating objects that are used as dependencies for other hooks + +### Performance Note + +Don't overuse these hooks! They come with their own overhead. Only use them when you have identified a genuine performance issue. + +### ~~Deprecated Methods~~ + +Note that ~~class-based lifecycle methods~~ like \`componentDidMount\` are now replaced by the \`useEffect\` hook in modern React development.`, + }, + ], + ], +]); + +export const userMessageTexts = Array.from(mockMessages.keys()); + +export const suggestions = [ + { icon: BarChartIcon, text: "Analyze data", color: "#76d0eb" }, + { icon: BoxIcon, text: "Surprise me", color: "#76d0eb" }, + { icon: NotepadTextIcon, text: "Summarize text", color: "#ea8444" }, + { icon: CodeSquareIcon, text: "Code", color: "#6c71ff" }, + { icon: GraduationCapIcon, text: "Get advice", color: "#76d0eb" }, + { icon: null, text: "More" }, +]; + +export const mockResponses = [ + "That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.", + "I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.", + "This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...", + "Great choice of topic! This is something that many developers encounter. The approach I'd suggest is to start with the fundamentals and then build up to more complex scenarios.", + "That's definitely worth exploring. From what I can see, the best way to handle this is to consider both the theoretical aspects and practical implementation details.", +]; + +export const getLastUserMessageText = (messages: UIMessage[]) => { + const lastUserMessage = [...messages] + .reverse() + .find((msg) => msg.role === "user"); + const textPart = lastUserMessage?.parts.find((p) => p.type === "text"); + return textPart && "text" in textPart ? textPart.text : ""; +}; diff --git a/packages/examples/src/demo-chatgpt.tsx b/packages/examples/src/demo-chatgpt.tsx index 01b48350..98918830 100644 --- a/packages/examples/src/demo-chatgpt.tsx +++ b/packages/examples/src/demo-chatgpt.tsx @@ -1,13 +1,7 @@ "use client"; -import { - Branch, - BranchMessages, - BranchNext, - BranchPage, - BranchPrevious, - BranchSelector, -} from "@repo/elements/branch"; +import { useChat } from "@ai-sdk/react"; +import { StaticChatTransport } from "@loremllm/transport"; import { Conversation, ConversationContent, @@ -35,6 +29,13 @@ import { SourcesTrigger, } from "@repo/elements/sources"; import { Suggestion, Suggestions } from "@repo/elements/suggestion"; +import { + Tool, + ToolContent, + ToolHeader, + ToolInput, + ToolOutput, +} from "@repo/elements/tool"; import { DropdownMenu, DropdownMenuContent, @@ -45,509 +46,75 @@ import { cn } from "@repo/shadcn-ui/lib/utils"; import type { ToolUIPart } from "ai"; import { AudioWaveformIcon, - BarChartIcon, - BoxIcon, CameraIcon, - CodeSquareIcon, FileIcon, GlobeIcon, - GraduationCapIcon, ImageIcon, - NotepadTextIcon, PaperclipIcon, ScreenShareIcon, } from "lucide-react"; -import { nanoid } from "nanoid"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; - -type MessageType = { - key: string; - from: "user" | "assistant"; - sources?: { href: string; title: string }[]; - versions: { - id: string; - content: string; - }[]; - reasoning?: { - content: string; - duration: number; - }; - tools?: { - name: string; - description: string; - status: ToolUIPart["state"]; - parameters: Record; - result: string | undefined; - error: string | undefined; - }[]; - avatar: string; - name: string; - isReasoningComplete?: boolean; - isContentComplete?: boolean; - isReasoningStreaming?: boolean; -}; - -const mockMessages: MessageType[] = [ - { - avatar: "", - key: nanoid(), - from: "user", - versions: [ - { - id: nanoid(), - content: "Can you explain how to use React hooks effectively?", - }, - ], - name: "Hayden Bleasel", - }, - { - avatar: "", - key: nanoid(), - from: "assistant", - sources: [ - { - href: "https://react.dev/reference/react", - title: "React Documentation", - }, - { - href: "https://react.dev/reference/react-dom", - title: "React DOM Documentation", - }, - ], - tools: [ - { - name: "mcp", - description: "Searching React documentation", - status: "input-available", - parameters: { - query: "React hooks best practices", - source: "react.dev", - }, - result: `{ - "query": "React hooks best practices", - "results": [ - { - "title": "Rules of Hooks", - "url": "https://react.dev/warnings/invalid-hook-call-warning", - "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." - }, - { - "title": "useState Hook", - "url": "https://react.dev/reference/react/useState", - "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." - }, - { - "title": "useEffect Hook", - "url": "https://react.dev/reference/react/useEffect", - "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." - } - ] -}`, - error: undefined, - }, - ], - versions: [ - { - id: nanoid(), - content: `# React Hooks Best Practices - -React hooks are a powerful feature that let you use state and other React features without writing classes. Here are some tips for using them effectively: - -## Rules of Hooks - -1. **Only call hooks at the top level** of your component or custom hooks -2. **Don't call hooks inside loops, conditions, or nested functions** - -## Common Hooks - -- **useState**: For local component state -- **useEffect**: For side effects like data fetching -- **useContext**: For consuming context -- **useReducer**: For complex state logic -- **useCallback**: For memoizing functions -- **useMemo**: For memoizing values - -## Example of useState and useEffect - -\`\`\`jsx -function ProfilePage({ userId }) { - const [user, setUser] = useState(null); - - useEffect(() => { - // This runs after render and when userId changes - fetchUser(userId).then(userData => { - setUser(userData); - }); - }, [userId]); - - return user ? : ; -} -\`\`\` - -Would you like me to explain any specific hook in more detail?`, - }, - ], - name: "OpenAI", - }, - { - avatar: "", - key: nanoid(), - from: "user", - versions: [ - { - id: nanoid(), - content: - "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", - }, - { - id: nanoid(), - content: - "I'm particularly interested in understanding the performance implications of useCallback and useMemo. Could you break down when each is most appropriate?", - }, - { - id: nanoid(), - content: - "Thanks for the overview! Could you dive deeper into the specific use cases where useCallback and useMemo make the biggest difference in React applications?", - }, - ], - name: "Hayden Bleasel", - }, - { - avatar: "", - key: nanoid(), - from: "assistant", - reasoning: { - content: `The user is asking for a detailed explanation of useCallback and useMemo. I should provide a clear and concise explanation of each hook's purpose and how they differ. - -The useCallback hook is used to memoize functions to prevent unnecessary re-renders of child components that receive functions as props. - -The useMemo hook is used to memoize values to avoid expensive recalculations on every render. - -Both hooks help with performance optimization, but they serve different purposes.`, - duration: 10, - }, - versions: [ - { - id: nanoid(), - content: `## useCallback vs useMemo - -Both hooks help with *performance optimization*, but they serve different purposes: - -### useCallback - -\`useCallback\` memoizes **functions** to prevent unnecessary re-renders of child components that receive functions as props. - -\`\`\`jsx -// Without useCallback - a new function is created on every render -const handleClick = () => { - console.log(count); -}; - -// With useCallback - the function is only recreated when dependencies change -const handleClick = useCallback(() => { - console.log(count); -}, [count]); -\`\`\` - -### useMemo - -\`useMemo\` memoizes **values** to avoid expensive recalculations on every render. - -\`\`\`jsx -// Without useMemo - expensive calculation runs on every render -const sortedList = expensiveSort(items); - -// With useMemo - calculation only runs when items change -const sortedList = useMemo(() => expensiveSort(items), [items]); -\`\`\` - -### When to use which? - -- Use **useCallback** when: - - Passing callbacks to optimized child components that rely on reference equality - - Working with event handlers that you pass to child components - -- Use **useMemo** when: - - You have computationally expensive calculations - - You want to avoid recreating objects that are used as dependencies for other hooks - -### Performance Note - -Don't overuse these hooks! They come with their own overhead. Only use them when you have identified a genuine performance issue. - -### ~~Deprecated Methods~~ - -Note that ~~class-based lifecycle methods~~ like \`componentDidMount\` are now replaced by the \`useEffect\` hook in modern React development.`, - }, - ], - name: "OpenAI", - }, -]; - -const suggestions = [ - { icon: BarChartIcon, text: "Analyze data", color: "#76d0eb" }, - { icon: BoxIcon, text: "Surprise me", color: "#76d0eb" }, - { icon: NotepadTextIcon, text: "Summarize text", color: "#ea8444" }, - { icon: CodeSquareIcon, text: "Code", color: "#6c71ff" }, - { icon: GraduationCapIcon, text: "Get advice", color: "#76d0eb" }, - { icon: null, text: "More" }, -]; - -const mockResponses = [ - "That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.", - "I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.", - "This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...", - "Great choice of topic! This is something that many developers encounter. The approach I'd suggest is to start with the fundamentals and then build up to more complex scenarios.", - "That's definitely worth exploring. From what I can see, the best way to handle this is to consider both the theoretical aspects and practical implementation details.", -]; +import { + getLastUserMessageText, + mockMessages, + mockResponses, + suggestions, + userMessageTexts, +} from "./demo-chat-data"; const Example = () => { const [text, setText] = useState(""); const [useWebSearch, setUseWebSearch] = useState(false); const [useMicrophone, setUseMicrophone] = useState(false); - const [status, setStatus] = useState< - "submitted" | "streaming" | "ready" | "error" - >("ready"); - const [messages, setMessages] = useState([]); - const [streamingMessageId, setStreamingMessageId] = useState( - null - ); - const streamReasoning = async ( - messageKey: string, - versionId: string, - reasoningContent: string - ) => { - const words = reasoningContent.split(" "); - let currentContent = ""; - - for (let i = 0; i < words.length; i++) { - currentContent += (i > 0 ? " " : "") + words[i]; - - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - reasoning: msg.reasoning - ? { ...msg.reasoning, content: currentContent } - : undefined, - }; - } - return msg; - }) - ); - - await new Promise((resolve) => - setTimeout(resolve, Math.random() * 30 + 20) - ); - } - - // Mark reasoning as complete - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - isReasoningComplete: true, - isReasoningStreaming: false, - }; - } - return msg; - }) - ); - }; - - const streamContent = async ( - messageKey: string, - versionId: string, - content: string - ) => { - const words = content.split(" "); - let currentContent = ""; - - for (let i = 0; i < words.length; i++) { - currentContent += (i > 0 ? " " : "") + words[i]; - - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - versions: msg.versions.map((v) => - v.id === versionId ? { ...v, content: currentContent } : v - ), - }; - } - return msg; - }) - ); - - await new Promise((resolve) => - setTimeout(resolve, Math.random() * 50 + 25) - ); - } - - // Mark content as complete - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { ...msg, isContentComplete: true }; + const { messages, sendMessage, status } = useChat({ + id: "demo-chatgpt", + transport: new StaticChatTransport({ + chunkDelayMs: [20, 50], + async *mockResponse({ messages }) { + const lastUserMessageText = getLastUserMessageText(messages); + + // If we already have a mock response for the user message: + const assistantParts = mockMessages.get(lastUserMessageText); + if (assistantParts) { + for (const part of assistantParts) yield part; + return; } - return msg; - }) - ); - }; - - const streamResponse = useCallback( - async ( - messageKey: string, - versionId: string, - content: string, - reasoning?: { content: string; duration: number } - ) => { - setStatus("streaming"); - setStreamingMessageId(versionId); - - // First stream the reasoning if it exists - if (reasoning) { - await streamReasoning(messageKey, versionId, reasoning.content); - await new Promise((resolve) => setTimeout(resolve, 500)); // Pause between reasoning and content - } - - // Then stream the content - await streamContent(messageKey, versionId, content); - - setStatus("ready"); - setStreamingMessageId(null); - }, - [] - ); - - const streamMessage = useCallback( - async (message: MessageType) => { - if (message.from === "user") { - setMessages((prev) => [...prev, message]); - return; - } - - // Add empty assistant message with reasoning structure - const newMessage = { - ...message, - versions: message.versions.map((v) => ({ ...v, content: "" })), - reasoning: message.reasoning - ? { ...message.reasoning, content: "" } - : undefined, - isReasoningComplete: false, - isContentComplete: false, - isReasoningStreaming: !!message.reasoning, - }; - - setMessages((prev) => [...prev, newMessage]); - // Get the first version for streaming - const firstVersion = message.versions[0]; - if (!firstVersion) return; - - // Stream the response - await streamResponse( - newMessage.key, - firstVersion.id, - firstVersion.content, - message.reasoning - ); - }, - [streamResponse] - ); - - const addUserMessage = useCallback( - (content: string) => { - const userMessage: MessageType = { - key: `user-${Date.now()}`, - from: "user", - versions: [ - { - id: `user-${Date.now()}`, - content, - }, - ], - name: "User", - avatar: "", - }; - - setMessages((prev) => [...prev, userMessage]); - - setTimeout(() => { - const assistantMessageKey = `assistant-${Date.now()}`; - const assistantMessageId = `version-${Date.now()}`; + // Default response for user messages that aren't structurally defined const randomResponse = mockResponses[Math.floor(Math.random() * mockResponses.length)]; - // Create reasoning for some responses - const shouldHaveReasoning = Math.random() > 0.5; - const reasoning = shouldHaveReasoning - ? { - content: - "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", - duration: 3, - } - : undefined; + if (Math.random() > 0.5) { + yield { + type: "reasoning", + text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", + }; + } - const assistantMessage: MessageType = { - key: assistantMessageKey, - from: "assistant", - versions: [ - { - id: assistantMessageId, - content: "", - }, - ], - name: "Assistant", - avatar: "", - reasoning: reasoning ? { ...reasoning, content: "" } : undefined, - isReasoningComplete: false, - isContentComplete: false, - isReasoningStreaming: !!reasoning, + yield { + type: "text", + text: randomResponse, }; - - setMessages((prev) => [...prev, assistantMessage]); - streamResponse( - assistantMessageKey, - assistantMessageId, - randomResponse, - reasoning - ); - }, 500); + }, + }), + initialMessages: [], + onFinish: ({ messages }) => { + // When the last message is the first demo message, send the last demo message + const lastUserMessageText = getLastUserMessageText(messages); + if (lastUserMessageText === userMessageTexts[0]) { + sendMessage({ text: userMessageTexts[userMessageTexts.length - 1] }); + } }, - [streamResponse] - ); + }); + // Initialize with first user message useEffect(() => { - // Reset state on mount to ensure fresh component - setMessages([]); - - const processMessages = async () => { - for (let i = 0; i < mockMessages.length; i++) { - await streamMessage(mockMessages[i]); - - if (i < mockMessages.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - }; - - // Small delay to ensure state is reset before starting - const timer = setTimeout(() => { - processMessages(); - }, 100); - - // Cleanup function to cancel any ongoing operations - return () => { - clearTimeout(timer); - setMessages([]); - }; - }, [streamMessage]); + if (messages.length === 0 && userMessageTexts.length > 0) { + sendMessage({ text: userMessageTexts[0] }); + } + }, [messages.length, sendMessage, userMessageTexts]); const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); @@ -557,8 +124,7 @@ const Example = () => { return; } - setStatus("submitted"); - addUserMessage(message.text || "Sent with attachments"); + sendMessage({ text: message.text || "Sent with attachments" }); setText(""); }; @@ -569,73 +135,92 @@ const Example = () => { }; const handleSuggestionClick = (suggestion: string) => { - setStatus("submitted"); - addUserMessage(suggestion); + sendMessage({ text: suggestion }); }; return (
- {messages.map(({ versions, ...message }) => ( - - - {versions.map((version) => ( - -
- {message.sources?.length && ( - - - - {message.sources.map((source) => ( - - ))} - - - )} - {message.reasoning && ( - { + const sources = message.parts.filter( + (p) => p.type === "source-url" + ); + const reasoningParts = message.parts.filter( + (p) => p.type === "reasoning" + ); + const toolParts = message.parts.filter((p) => + p.type.startsWith("tool-") + ); + const textParts = message.parts.filter((p) => p.type === "text"); + + return ( + +
+ {sources.length > 0 && ( + + + + {sources.map((source, i) => ( + + ))} + + + )} + {toolParts.map((toolPart, i) => { + if (toolPart.type.startsWith("tool-")) { + const tool = toolPart as ToolUIPart; + return ( + + + + + + + + ); + } + return null; + })} + {reasoningParts.map((reasoningPart, i) => + - {message.reasoning.content} + {reasoningPart.text} + )} + {textParts.map((textPart, i) => ( + - {version.content} - - )} -
-
- ))} - - {versions.length > 1 && ( - - - - - - )} - - ))} + key={`${message.id}-text-${i}`} + > + {textPart.text} + + ))} +
+
+ ); + })}
diff --git a/packages/examples/src/demo-claude.tsx b/packages/examples/src/demo-claude.tsx index 67a88fe8..1b80df49 100644 --- a/packages/examples/src/demo-claude.tsx +++ b/packages/examples/src/demo-claude.tsx @@ -653,7 +653,7 @@ const Example = () => {
=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@3.0.16': + resolution: {integrity: sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@3.1.0-beta.7': resolution: {integrity: sha512-9D7UvfrOqvvLqIKM19hg4xnyd3+RLOzMOy0yJ5JZRg9foexNf5fLVpHSN4hy+6m3cHTUhJ02l3DfbdwKf/ww+w==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + '@ai-sdk/provider@2.1.0-beta.5': resolution: {integrity: sha512-gu/aV+8iTb0IjHmFO6eDuevp4E0fbtXqSj0RcKPttgedAbWY3uMe+vVgfe/j3bfbQfN7FjGkFDLS0xIirga7pA==} engines: {node: '>=18'} + '@ai-sdk/react@2.0.87': + resolution: {integrity: sha512-uuM/FU2bT+DDQzL6YcwdQWZ5aKdT0QYsZzCNwM4jag4UQkryYJJ+CBpo2u3hZr4PaIIuL7TZzGMCzDN/UigQ9Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.25.76 || ^4.1.8 + peerDependenciesMeta: + zod: + optional: true + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1122,6 +1154,11 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@loremllm/transport@0.4.3': + resolution: {integrity: sha512-YKoYfmGeFEMeE3St+U9OuhI/2wWkafYOFZlAIOGmszjBpt8474HXhvveCNdGgt1LkS2s20W6uSd0UuAmrH4Ltw==} + peerDependencies: + ai: ^5.x.x + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2633,6 +2670,12 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ai@5.0.87: + resolution: {integrity: sha512-9Cjx7o8IY9zAczigX0Tk/BaQwjPe/M6DpEjejKSBNrf8mOPIvyM+pJLqJSC10IsKci3FPsnaizJeJhoetU1Wfw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ai@5.1.0-beta.22: resolution: {integrity: sha512-7z/PMSEQRes1aXywA5POKhoDxXD25CAT7Al2ObYFiQPudhL8GXd9wlXcbUBaz1+Smg4WHKR4f83iOv9jwyubvQ==} engines: {node: '>=18'} @@ -5264,6 +5307,11 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + swr@2.3.6: + resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -5281,6 +5329,10 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiktok-video-element@0.1.1: resolution: {integrity: sha512-BaiVzvNz2UXDKTdSrXzrNf4q6Ecc+/utYUh7zdEu2jzYcJVDoqYbVfUl0bCfMoOeeAqg28vD/yN63Y3E9jOrlA==} @@ -5846,6 +5898,20 @@ snapshots: '@vercel/oidc': 3.0.3 zod: 4.1.12 + '@ai-sdk/gateway@2.0.6(zod@4.1.12)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.16(zod@4.1.12) + '@vercel/oidc': 3.0.3 + zod: 4.1.12 + + '@ai-sdk/provider-utils@3.0.16(zod@4.1.12)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 4.1.12 + '@ai-sdk/provider-utils@3.1.0-beta.7(zod@4.1.12)': dependencies: '@ai-sdk/provider': 2.1.0-beta.5 @@ -5853,10 +5919,24 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.1.12 + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + '@ai-sdk/provider@2.1.0-beta.5': dependencies: json-schema: 0.4.0 + '@ai-sdk/react@2.0.87(react@19.2.0)(zod@4.1.12)': + dependencies: + '@ai-sdk/provider-utils': 3.0.16(zod@4.1.12) + ai: 5.0.87(zod@4.1.12) + react: 19.2.0 + swr: 2.3.6(react@19.2.0) + throttleit: 2.1.0 + optionalDependencies: + zod: 4.1.12 + '@alloc/quick-lru@5.2.0': {} '@antfu/install-pkg@1.1.0': @@ -6646,6 +6726,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@loremllm/transport@0.4.3(ai@5.1.0-beta.22(zod@4.1.12))': + dependencies: + ai: 5.1.0-beta.22(zod@4.1.12) + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.4 @@ -8069,7 +8153,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vercel/analytics@1.5.0(next@16.0.0(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': + '@vercel/analytics@1.5.0(next@16.0.0(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)': optionalDependencies: next: 16.0.0(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 @@ -8089,7 +8173,7 @@ snapshots: path-to-regexp: 6.2.1 semver: 7.7.2 optionalDependencies: - '@vercel/analytics': 1.5.0(next@16.0.0(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + '@vercel/analytics': 1.5.0(next@16.0.0(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) '@vercel/speed-insights': 1.2.0(next@16.0.0(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) next: 16.0.0(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 @@ -8259,6 +8343,14 @@ snapshots: agent-base@7.1.4: {} + ai@5.0.87(zod@4.1.12): + dependencies: + '@ai-sdk/gateway': 2.0.6(zod@4.1.12) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.16(zod@4.1.12) + '@opentelemetry/api': 1.9.0 + zod: 4.1.12 + ai@5.1.0-beta.22(zod@4.1.12): dependencies: '@ai-sdk/gateway': 1.1.0-beta.16(zod@4.1.12) @@ -11539,6 +11631,12 @@ snapshots: dependencies: has-flag: 4.0.0 + swr@2.3.6(react@19.2.0): + dependencies: + dequal: 2.0.3 + react: 19.2.0 + use-sync-external-store: 1.5.0(react@19.2.0) + symbol-tree@3.2.4: optional: true @@ -11550,6 +11648,8 @@ snapshots: term-size@2.2.1: {} + throttleit@2.1.0: {} + tiktok-video-element@0.1.1: {} tiny-invariant@1.3.3: {} From 23774367c0816c3dec69c4d67d384581d1a90d77 Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Tue, 4 Nov 2025 06:50:11 -0800 Subject: [PATCH 2/9] update other demos --- packages/examples/src/demo-chat-data.tsx | 58 +- packages/examples/src/demo-chatgpt.tsx | 22 +- packages/examples/src/demo-claude.tsx | 697 +++++------------------ packages/examples/src/demo-grok.tsx | 668 +++++----------------- 4 files changed, 312 insertions(+), 1133 deletions(-) diff --git a/packages/examples/src/demo-chat-data.tsx b/packages/examples/src/demo-chat-data.tsx index dc665d5b..9206bdd3 100644 --- a/packages/examples/src/demo-chat-data.tsx +++ b/packages/examples/src/demo-chat-data.tsx @@ -27,35 +27,35 @@ export const mockMessages = new Map([ url: "https://react.dev/reference/react-dom", title: "React DOM Documentation", }, - // { - // type: "tool-mcp", - // toolCallId: nanoid(), - // state: "output-available" as const, - // input: { - // query: "React hooks best practices", - // source: "react.dev", - // }, - // output: `{ - // "query": "React hooks best practices", - // "results": [ - // { - // "title": "Rules of Hooks", - // "url": "https://react.dev/warnings/invalid-hook-call-warning", - // "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." - // }, - // { - // "title": "useState Hook", - // "url": "https://react.dev/reference/react/useState", - // "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." - // }, - // { - // "title": "useEffect Hook", - // "url": "https://react.dev/reference/react/useEffect", - // "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." - // } - // ] - // }`, - // }, + { + type: "tool-mcp", + toolCallId: nanoid(), + state: "output-available" as const, + input: { + query: "React hooks best practices", + source: "react.dev", + }, + output: `{ + "query": "React hooks best practices", + "results": [ + { + "title": "Rules of Hooks", + "url": "https://react.dev/warnings/invalid-hook-call-warning", + "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." + }, + { + "title": "useState Hook", + "url": "https://react.dev/reference/react/useState", + "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." + }, + { + "title": "useEffect Hook", + "url": "https://react.dev/reference/react/useEffect", + "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." + } + ] + }`, + }, { type: "text", text: `# React Hooks Best Practices diff --git a/packages/examples/src/demo-chatgpt.tsx b/packages/examples/src/demo-chatgpt.tsx index 98918830..09cd7643 100644 --- a/packages/examples/src/demo-chatgpt.tsx +++ b/packages/examples/src/demo-chatgpt.tsx @@ -68,7 +68,7 @@ const Example = () => { const [useWebSearch, setUseWebSearch] = useState(false); const [useMicrophone, setUseMicrophone] = useState(false); - const { messages, sendMessage, status } = useChat({ + const { messages, sendMessage } = useChat({ id: "demo-chatgpt", transport: new StaticChatTransport({ chunkDelayMs: [20, 50], @@ -195,17 +195,15 @@ const Example = () => { } return null; })} - {reasoningParts.map((reasoningPart, i) => - - - - {reasoningPart.text} - - - )} + {reasoningParts.map((reasoningPart, i) => ( + + + {reasoningPart.text} + + ))} {textParts.map((textPart, i) => ( ; - result: string | undefined; - error: string | undefined; - }[]; - avatar: string; - name: string; - isReasoningComplete?: boolean; - isContentComplete?: boolean; - isReasoningStreaming?: boolean; -}; - -const mockMessages: MessageType[] = [ - { - key: nanoid(), - from: "user", - versions: [ - { - id: nanoid(), - content: "Can you explain how to use React hooks effectively?", - }, - ], - avatar: "https://github.com/haydenbleasel.png", - name: "Hayden Bleasel", - }, - { - key: nanoid(), - from: "assistant", - sources: [ - { - href: "https://react.dev/reference/react", - title: "React Documentation", - }, - { - href: "https://react.dev/reference/react-dom", - title: "React DOM Documentation", - }, - ], - tools: [ - { - name: "mcp", - description: "Searching React documentation", - status: "input-available", - parameters: { - query: "React hooks best practices", - source: "react.dev", - }, - result: `{ - "query": "React hooks best practices", - "results": [ - { - "title": "Rules of Hooks", - "url": "https://react.dev/warnings/invalid-hook-call-warning", - "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." - }, - { - "title": "useState Hook", - "url": "https://react.dev/reference/react/useState", - "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." - }, - { - "title": "useEffect Hook", - "url": "https://react.dev/reference/react/useEffect", - "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." - } - ] -}`, - error: undefined, - }, - ], - versions: [ - { - id: nanoid(), - content: `# React Hooks Best Practices - -React hooks are a powerful feature that let you use state and other React features without writing classes. Here are some tips for using them effectively: - -## Rules of Hooks - -1. **Only call hooks at the top level** of your component or custom hooks -2. **Don't call hooks inside loops, conditions, or nested functions** - -## Common Hooks - -- **useState**: For local component state -- **useEffect**: For side effects like data fetching -- **useContext**: For consuming context -- **useReducer**: For complex state logic -- **useCallback**: For memoizing functions -- **useMemo**: For memoizing values - -## Example of useState and useEffect - -\`\`\`jsx -function ProfilePage({ userId }) { - const [user, setUser] = useState(null); - - useEffect(() => { - // This runs after render and when userId changes - fetchUser(userId).then(userData => { - setUser(userData); - }); - }, [userId]); - - return user ? : ; -} -\`\`\` - -Would you like me to explain any specific hook in more detail?`, - }, - ], - avatar: "https://github.com/openai.png", - name: "OpenAI", - }, - { - key: nanoid(), - from: "user", - versions: [ - { - id: nanoid(), - content: - "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", - }, - { - id: nanoid(), - content: - "I'm particularly interested in understanding the performance implications of useCallback and useMemo. Could you break down when each is most appropriate?", - }, - { - id: nanoid(), - content: - "Thanks for the overview! Could you dive deeper into the specific use cases where useCallback and useMemo make the biggest difference in React applications?", - }, - ], - avatar: "https://github.com/haydenbleasel.png", - name: "Hayden Bleasel", - }, - { - key: nanoid(), - from: "assistant", - reasoning: { - content: `The user is asking for a detailed explanation of useCallback and useMemo. I should provide a clear and concise explanation of each hook's purpose and how they differ. - -The useCallback hook is used to memoize functions to prevent unnecessary re-renders of child components that receive functions as props. - -The useMemo hook is used to memoize values to avoid expensive recalculations on every render. - -Both hooks help with performance optimization, but they serve different purposes.`, - duration: 10, - }, - versions: [ - { - id: nanoid(), - content: `## useCallback vs useMemo - -Both hooks help with _performance optimization_, but they serve different purposes: - -### useCallback - -\`useCallback\` memoizes **functions** to prevent unnecessary re-renders of child components that receive functions as props. - -\`\`\`jsx -// Without useCallback - a new function is created on every render -const handleClick = () => { - console.log(count); -}; - -// With useCallback - the function is only recreated when dependencies change -const handleClick = useCallback(() => { - console.log(count); -}, [count]); -\`\`\` - -### useMemo - -\`useMemo\` memoizes __values__ to avoid expensive recalculations on every render. - -\`\`\`jsx -// Without useMemo - expensive calculation runs on every render -const sortedList = expensiveSort(items); - -// With useMemo - calculation only runs when items change -const sortedList = useMemo(() => expensiveSort(items), [items]); -\`\`\` - -### When to use which? - -- Use **useCallback** when: - - Passing callbacks to optimized child components that rely on reference equality - - Working with event handlers that you pass to child components - -- Use **useMemo** when: - - You have computationally expensive calculations - - You want to avoid recreating objects that are used as dependencies for other hooks - -### Performance Note - -Don't overuse these hooks! They come with their own overhead. Only use them when you have identified a genuine performance issue. - -### ~~Common Mistakes~~ - -Avoid these ~~anti-patterns~~ when using hooks: -- ~~Calling hooks conditionally~~ - Always call hooks at the top level -- Using \`useEffect\` without proper dependency arrays`, - }, - ], - avatar: "https://github.com/openai.png", - name: "OpenAI", - }, -]; +import { + getLastUserMessageText, + mockMessages, + mockResponses, + userMessageTexts, +} from "./demo-chat-data"; const models = [ { id: "claude-3-opus", name: "Claude 3 Opus" }, @@ -293,261 +73,57 @@ const models = [ { id: "claude-3-haiku", name: "Claude 3 Haiku" }, ]; -const mockResponses = [ - "That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.", - "I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.", - "This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...", - "Great choice of topic! This is something that many developers encounter. The approach I'd suggest is to start with the fundamentals and then build up to more complex scenarios.", - "That's definitely worth exploring. From what I can see, the best way to handle this is to consider both the theoretical aspects and practical implementation details.", -]; - const Example = () => { const [model, setModel] = useState(models[0].id); const [text, setText] = useState(""); - const [useWebSearch, setUseWebSearch] = useState(false); - const [useMicrophone, setUseMicrophone] = useState(false); - const [status, setStatus] = useState< - "submitted" | "streaming" | "ready" | "error" - >("ready"); - const [messages, setMessages] = useState([]); - const [streamingMessageId, setStreamingMessageId] = useState( - null - ); - - const streamReasoning = async ( - messageKey: string, - versionId: string, - reasoningContent: string - ) => { - const words = reasoningContent.split(" "); - let currentContent = ""; - for (let i = 0; i < words.length; i++) { - currentContent += (i > 0 ? " " : "") + words[i]; - - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - reasoning: msg.reasoning - ? { ...msg.reasoning, content: currentContent } - : undefined, - }; - } - return msg; - }) - ); - - await new Promise((resolve) => - setTimeout(resolve, Math.random() * 30 + 20) - ); - } - - // Mark reasoning as complete - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - isReasoningComplete: true, - isReasoningStreaming: false, - }; - } - return msg; - }) - ); - }; - - const streamContent = async ( - messageKey: string, - versionId: string, - content: string - ) => { - const words = content.split(" "); - let currentContent = ""; - - for (let i = 0; i < words.length; i++) { - currentContent += (i > 0 ? " " : "") + words[i]; - - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - versions: msg.versions.map((v) => - v.id === versionId ? { ...v, content: currentContent } : v - ), - }; - } - return msg; - }) - ); - - await new Promise((resolve) => - setTimeout(resolve, Math.random() * 50 + 25) - ); - } - - // Mark content as complete - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { ...msg, isContentComplete: true }; + const { messages, sendMessage, status } = useChat({ + id: "demo-claude", + transport: new StaticChatTransport({ + chunkDelayMs: [20, 50], + async *mockResponse({ messages }) { + const lastUserMessageText = getLastUserMessageText(messages); + + // If we already have a mock response for the user message: + const assistantParts = mockMessages.get(lastUserMessageText); + if (assistantParts) { + for (const part of assistantParts) yield part; + return; } - return msg; - }) - ); - }; - const streamResponse = useCallback( - async ( - messageKey: string, - versionId: string, - content: string, - reasoning?: { content: string; duration: number } - ) => { - setStatus("streaming"); - setStreamingMessageId(versionId); - - // First stream the reasoning if it exists - if (reasoning) { - await streamReasoning(messageKey, versionId, reasoning.content); - await new Promise((resolve) => setTimeout(resolve, 500)); // Pause between reasoning and content - } - - // Then stream the content - await streamContent(messageKey, versionId, content); - - setStatus("ready"); - setStreamingMessageId(null); - }, - [] - ); - - const streamMessage = useCallback( - async (message: MessageType) => { - if (message.from === "user") { - setMessages((prev) => [...prev, message]); - return; - } - - // Add empty assistant message with reasoning structure - const newMessage = { - ...message, - versions: message.versions.map((v) => ({ ...v, content: "" })), - reasoning: message.reasoning - ? { ...message.reasoning, content: "" } - : undefined, - isReasoningComplete: false, - isContentComplete: false, - isReasoningStreaming: !!message.reasoning, - }; - - setMessages((prev) => [...prev, newMessage]); - - // Get the first version for streaming - const firstVersion = message.versions[0]; - if (!firstVersion) return; - - // Stream the response - await streamResponse( - newMessage.key, - firstVersion.id, - firstVersion.content, - message.reasoning - ); - }, - [streamResponse] - ); - - const addUserMessage = useCallback( - (content: string) => { - const userMessage: MessageType = { - key: `user-${Date.now()}`, - from: "user", - versions: [ - { - id: `user-${Date.now()}`, - content, - }, - ], - avatar: "https://github.com/haydenbleasel.png", - name: "User", - }; - - setMessages((prev) => [...prev, userMessage]); - - setTimeout(() => { - const assistantMessageKey = `assistant-${Date.now()}`; - const assistantMessageId = `version-${Date.now()}`; + // Default response for user messages that aren't structurally defined const randomResponse = mockResponses[Math.floor(Math.random() * mockResponses.length)]; - // Create reasoning for some responses - const shouldHaveReasoning = Math.random() > 0.5; - const reasoning = shouldHaveReasoning - ? { - content: - "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", - duration: 3, - } - : undefined; + if (Math.random() > 0.5) { + yield { + type: "reasoning", + text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", + }; + } - const assistantMessage: MessageType = { - key: assistantMessageKey, - from: "assistant", - versions: [ - { - id: assistantMessageId, - content: "", - }, - ], - avatar: "https://github.com/openai.png", - name: "Assistant", - reasoning: reasoning ? { ...reasoning, content: "" } : undefined, - isReasoningComplete: false, - isContentComplete: false, - isReasoningStreaming: !!reasoning, + yield { + type: "text", + text: randomResponse, }; - - setMessages((prev) => [...prev, assistantMessage]); - streamResponse( - assistantMessageKey, - assistantMessageId, - randomResponse, - reasoning - ); - }, 500); + }, + }), + initialMessages: [], + onFinish: ({ messages }) => { + // When the last message is the first demo message, send the last demo message + const lastUserMessageText = getLastUserMessageText(messages); + if (lastUserMessageText === userMessageTexts[0]) { + sendMessage({ text: userMessageTexts[userMessageTexts.length - 1] }); + } }, - [streamResponse] - ); + }); + // Initialize with first user message useEffect(() => { - // Reset state on mount to ensure fresh component - setMessages([]); - - const processMessages = async () => { - for (let i = 0; i < mockMessages.length; i++) { - await streamMessage(mockMessages[i]); - - if (i < mockMessages.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - }; - - // Small delay to ensure state is reset before starting - const timer = setTimeout(() => { - processMessages(); - }, 100); - - // Cleanup function to cancel any ongoing operations - return () => { - clearTimeout(timer); - setMessages([]); - }; - }, [streamMessage]); + if (messages.length === 0 && userMessageTexts.length > 0) { + sendMessage({ text: userMessageTexts[0] }); + } + }, [messages.length, sendMessage, userMessageTexts]); const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); @@ -557,8 +133,7 @@ const Example = () => { return; } - setStatus("submitted"); - addUserMessage(message.text || "Sent with attachments"); + sendMessage({ text: message.text || "Sent with attachments" }); setText(""); }; @@ -568,86 +143,102 @@ const Example = () => { }); }; - const handleSuggestionClick = (suggestion: string) => { - setStatus("submitted"); - addUserMessage(suggestion); - }; - return (
- {messages.map(({ versions, ...message }) => ( - - - {versions.map((version) => ( - -
- {message.sources?.length && ( - - - - {message.sources.map((source) => ( - - ))} - - - )} - {message.reasoning && ( - - - - {message.reasoning.content} - - - )} - {(message.from === "user" || - message.isReasoningComplete || - !message.reasoning) && ( - { + const sources = message.parts.filter( + (p) => p.type === "source-url" + ); + const reasoningParts = message.parts.filter( + (p) => p.type === "reasoning" + ); + const toolParts = message.parts.filter((p) => + p.type.startsWith("tool-") + ); + const textParts = message.parts.filter((p) => p.type === "text"); + + return ( + +
+ {sources.length > 0 && ( + + + + {sources.map((source, i) => ( + + ))} + + + )} + {toolParts.map((toolPart, i) => { + if (toolPart.type.startsWith("tool-")) { + const tool = toolPart as ToolUIPart; + return ( + -
- {message.from === "user" && ( - - )} -
- {version.content} -
-
- + + + + + +
+ ); + } + return null; + })} + {reasoningParts.map((reasoningPart, i) => ( + + + {reasoningPart.text} + + ))} + {textParts.map((textPart, i) => ( + - - ))} - - {versions.length > 1 && ( - - - - - - )} - - ))} + key={`${message.id}-text-${i}`} + > +
+ {message.role === "user" && ( + + )} +
+ {textPart.text} +
+
+
+ ))} +
+
+ ); + })} diff --git a/packages/examples/src/demo-grok.tsx b/packages/examples/src/demo-grok.tsx index e20b05ad..cf5dc748 100644 --- a/packages/examples/src/demo-grok.tsx +++ b/packages/examples/src/demo-grok.tsx @@ -1,13 +1,7 @@ "use client"; -import { - Branch, - BranchMessages, - BranchNext, - BranchPage, - BranchPrevious, - BranchSelector, -} from "@repo/elements/branch"; +import { useChat } from "@ai-sdk/react"; +import { StaticChatTransport } from "@loremllm/transport"; import { Conversation, ConversationContent, @@ -39,6 +33,13 @@ import { SourcesContent, SourcesTrigger, } from "@repo/elements/sources"; +import { + Tool, + ToolContent, + ToolHeader, + ToolInput, + ToolOutput, +} from "@repo/elements/tool"; import { DropdownMenu, DropdownMenuContent, @@ -58,496 +59,73 @@ import { ScreenShareIcon, SearchIcon, } from "lucide-react"; -import { nanoid } from "nanoid"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; - -type MessageType = { - key: string; - from: "user" | "assistant"; - sources?: { href: string; title: string }[]; - versions: { - id: string; - content: string; - }[]; - reasoning?: { - content: string; - duration: number; - }; - tools?: { - name: string; - description: string; - status: ToolUIPart["state"]; - parameters: Record; - result: string | undefined; - error: string | undefined; - }[]; - avatar: string; - name: string; - isReasoningComplete?: boolean; - isContentComplete?: boolean; - isReasoningStreaming?: boolean; -}; +import { + getLastUserMessageText, + mockMessages, + mockResponses, + userMessageTexts, +} from "./demo-chat-data"; const models = [ { id: "grok-3", name: "Grok-3" }, { id: "grok-2-1212", name: "Grok-2-1212" }, ]; -const mockMessages: MessageType[] = [ - { - avatar: "", - key: nanoid(), - from: "user", - versions: [ - { - id: nanoid(), - content: "Can you explain how to use React hooks effectively?", - }, - ], - name: "Hayden Bleasel", - }, - { - avatar: "", - key: nanoid(), - from: "assistant", - sources: [ - { - href: "https://react.dev/reference/react", - title: "React Documentation", - }, - { - href: "https://react.dev/reference/react-dom", - title: "React DOM Documentation", - }, - ], - tools: [ - { - name: "mcp", - description: "Searching React documentation", - status: "input-available", - parameters: { - query: "React hooks best practices", - source: "react.dev", - }, - result: `{ - "query": "React hooks best practices", - "results": [ - { - "title": "Rules of Hooks", - "url": "https://react.dev/warnings/invalid-hook-call-warning", - "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." - }, - { - "title": "useState Hook", - "url": "https://react.dev/reference/react/useState", - "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." - }, - { - "title": "useEffect Hook", - "url": "https://react.dev/reference/react/useEffect", - "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." - } - ] -}`, - error: undefined, - }, - ], - versions: [ - { - id: nanoid(), - content: `# React Hooks Best Practices - -React hooks are a powerful feature that let you use state and other React features without writing classes. Here are some tips for using them effectively: - -## Rules of Hooks - -1. **Only call hooks at the top level** of your component or custom hooks -2. **Don't call hooks inside loops, conditions, or nested functions** - -## Common Hooks - -- **useState**: For local component state -- **useEffect**: For side effects like data fetching -- **useContext**: For consuming context -- **useReducer**: For complex state logic -- **useCallback**: For memoizing functions -- **useMemo**: For memoizing values - -## Example of useState and useEffect - -\`\`\`jsx -function ProfilePage({ userId }) { - const [user, setUser] = useState(null); - - useEffect(() => { - // This runs after render and when userId changes - fetchUser(userId).then(userData => { - setUser(userData); - }); - }, [userId]); - - return user ? : ; -} -\`\`\` - -Would you like me to explain any specific hook in more detail?`, - }, - ], - name: "OpenAI", - }, - { - avatar: "", - key: nanoid(), - from: "user", - versions: [ - { - id: nanoid(), - content: - "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", - }, - { - id: nanoid(), - content: - "I'm particularly interested in understanding the performance implications of useCallback and useMemo. Could you break down when each is most appropriate?", - }, - { - id: nanoid(), - content: - "Thanks for the overview! Could you dive deeper into the specific use cases where useCallback and useMemo make the biggest difference in React applications?", - }, - ], - name: "Hayden Bleasel", - }, - { - avatar: "", - key: nanoid(), - from: "assistant", - reasoning: { - content: `The user is asking for a detailed explanation of useCallback and useMemo. I should provide a clear and concise explanation of each hook's purpose and how they differ. - -The useCallback hook is used to memoize functions to prevent unnecessary re-renders of child components that receive functions as props. - -The useMemo hook is used to memoize values to avoid expensive recalculations on every render. - -Both hooks help with performance optimization, but they serve different purposes.`, - duration: 10, - }, - versions: [ - { - id: nanoid(), - content: `## useCallback vs useMemo - -Both hooks help with **performance optimization**, but they serve _different purposes_: - -### useCallback - -\`useCallback\` memoizes __functions__ to prevent unnecessary re-renders of child components that receive functions as props. - -\`\`\`jsx -// Without useCallback - a new function is created on every render -const handleClick = () => { - console.log(count); -}; - -// With useCallback - the function is only recreated when dependencies change -const handleClick = useCallback(() => { - console.log(count); -}, [count]); -\`\`\` - -### useMemo - -\`useMemo\` memoizes *values* to avoid expensive recalculations on every render. - -\`\`\`jsx -// Without useMemo - expensive calculation runs on every render -const sortedList = expensiveSort(items); - -// With useMemo - calculation only runs when items change -const sortedList = useMemo(() => expensiveSort(items), [items]); -\`\`\` - -### When to use which? - -- Use **useCallback** when: - - Passing callbacks to optimized child components that rely on reference equality - - Working with event handlers that you pass to child components - -- Use **useMemo** when: - - You have computationally expensive calculations - - You want to avoid recreating objects that are used as dependencies for other hooks - -### Performance Note - -Don't overuse these hooks! They come with their own overhead. Only use them when you have identified a genuine performance issue. - -### ~~Legacy Patterns~~ - -Remember that these ~~outdated approaches~~ should be avoided: -- ~~Class components for simple state~~ - Use \`useState\` instead -- ~~Manual event listener cleanup~~ - Let \`useEffect\` handle it`, - }, - ], - name: "OpenAI", - }, -]; - -const mockResponses = [ - "That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.", - "I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.", - "This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...", - "Great choice of topic! This is something that many developers encounter. The approach I'd suggest is to start with the fundamentals and then build up to more complex scenarios.", - "That's definitely worth exploring. From what I can see, the best way to handle this is to consider both the theoretical aspects and practical implementation details.", -]; - const Example = () => { const [model, setModel] = useState(models[0].id); const [text, setText] = useState(""); const [useWebSearch, setUseWebSearch] = useState(false); const [useMicrophone, setUseMicrophone] = useState(false); - const [status, setStatus] = useState< - "submitted" | "streaming" | "ready" | "error" - >("ready"); - const [messages, setMessages] = useState([]); - const [streamingMessageId, setStreamingMessageId] = useState( - null - ); - const streamReasoning = async ( - messageKey: string, - versionId: string, - reasoningContent: string - ) => { - const words = reasoningContent.split(" "); - let currentContent = ""; - - for (let i = 0; i < words.length; i++) { - currentContent += (i > 0 ? " " : "") + words[i]; - - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - reasoning: msg.reasoning - ? { ...msg.reasoning, content: currentContent } - : undefined, - }; - } - return msg; - }) - ); - - await new Promise((resolve) => - setTimeout(resolve, Math.random() * 30 + 20) - ); - } - - // Mark reasoning as complete - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - isReasoningComplete: true, - isReasoningStreaming: false, - }; - } - return msg; - }) - ); - }; - - const streamContent = async ( - messageKey: string, - versionId: string, - content: string - ) => { - const words = content.split(" "); - let currentContent = ""; - - for (let i = 0; i < words.length; i++) { - currentContent += (i > 0 ? " " : "") + words[i]; - - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { - ...msg, - versions: msg.versions.map((v) => - v.id === versionId ? { ...v, content: currentContent } : v - ), - }; - } - return msg; - }) - ); - - await new Promise((resolve) => - setTimeout(resolve, Math.random() * 50 + 25) - ); - } - - // Mark content as complete - setMessages((prev) => - prev.map((msg) => { - if (msg.key === messageKey) { - return { ...msg, isContentComplete: true }; + const { messages, sendMessage } = useChat({ + id: "demo-grok", + transport: new StaticChatTransport({ + chunkDelayMs: [20, 50], + async *mockResponse({ messages }) { + const lastUserMessageText = getLastUserMessageText(messages); + + // If we already have a mock response for the user message: + const assistantParts = mockMessages.get(lastUserMessageText); + if (assistantParts) { + for (const part of assistantParts) yield part; + return; } - return msg; - }) - ); - }; - - const streamResponse = useCallback( - async ( - messageKey: string, - versionId: string, - content: string, - reasoning?: { content: string; duration: number } - ) => { - setStatus("streaming"); - setStreamingMessageId(versionId); - - // First stream the reasoning if it exists - if (reasoning) { - await streamReasoning(messageKey, versionId, reasoning.content); - await new Promise((resolve) => setTimeout(resolve, 500)); // Pause between reasoning and content - } - - // Then stream the content - await streamContent(messageKey, versionId, content); - - setStatus("ready"); - setStreamingMessageId(null); - }, - [] - ); - - const streamMessage = useCallback( - async (message: MessageType) => { - if (message.from === "user") { - setMessages((prev) => [...prev, message]); - return; - } - - // Add empty assistant message with reasoning structure - const newMessage = { - ...message, - versions: message.versions.map((v) => ({ ...v, content: "" })), - reasoning: message.reasoning - ? { ...message.reasoning, content: "" } - : undefined, - isReasoningComplete: false, - isContentComplete: false, - isReasoningStreaming: !!message.reasoning, - }; - - setMessages((prev) => [...prev, newMessage]); - - // Get the first version for streaming - const firstVersion = message.versions[0]; - if (!firstVersion) return; - // Stream the response - await streamResponse( - newMessage.key, - firstVersion.id, - firstVersion.content, - message.reasoning - ); - }, - [streamResponse] - ); - - const addUserMessage = useCallback( - (content: string) => { - const userMessage: MessageType = { - key: `user-${Date.now()}`, - from: "user", - versions: [ - { - id: `user-${Date.now()}`, - content, - }, - ], - avatar: "", - name: "User", - }; - - setMessages((prev) => [...prev, userMessage]); - - setTimeout(() => { - const assistantMessageKey = `assistant-${Date.now()}`; - const assistantMessageId = `version-${Date.now()}`; + // Default response for user messages that aren't structurally defined const randomResponse = mockResponses[Math.floor(Math.random() * mockResponses.length)]; - // Create reasoning for some responses - const shouldHaveReasoning = Math.random() > 0.5; - const reasoning = shouldHaveReasoning - ? { - content: - "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", - duration: 3, - } - : undefined; + if (Math.random() > 0.5) { + yield { + type: "reasoning", + text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", + }; + } - const assistantMessage: MessageType = { - key: assistantMessageKey, - from: "assistant", - versions: [ - { - id: assistantMessageId, - content: "", - }, - ], - name: "Assistant", - avatar: "", - reasoning: reasoning ? { ...reasoning, content: "" } : undefined, - isReasoningComplete: false, - isContentComplete: false, - isReasoningStreaming: !!reasoning, + yield { + type: "text", + text: randomResponse, }; - - setMessages((prev) => [...prev, assistantMessage]); - streamResponse( - assistantMessageKey, - assistantMessageId, - randomResponse, - reasoning - ); - }, 500); + }, + }), + initialMessages: [], + onFinish: ({ messages }) => { + // When the last message is the first demo message, send the last demo message + const lastUserMessageText = getLastUserMessageText(messages); + if (lastUserMessageText === userMessageTexts[0]) { + sendMessage({ text: userMessageTexts[userMessageTexts.length - 1] }); + } }, - [streamResponse] - ); + }); + // Initialize with first user message useEffect(() => { - // Reset state on mount to ensure fresh component - setMessages([]); - - const processMessages = async () => { - for (let i = 0; i < mockMessages.length; i++) { - await streamMessage(mockMessages[i]); - - if (i < mockMessages.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - }; - - // Small delay to ensure state is reset before starting - const timer = setTimeout(() => { - processMessages(); - }, 100); - - // Cleanup function to cancel any ongoing operations - return () => { - clearTimeout(timer); - setMessages([]); - }; - }, [streamMessage]); + if (messages.length === 0 && userMessageTexts.length > 0) { + sendMessage({ text: userMessageTexts[0] }); + } + }, [messages.length, sendMessage, userMessageTexts]); const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); @@ -557,8 +135,7 @@ const Example = () => { return; } - setStatus("submitted"); - addUserMessage(message.text || "Sent with attachments"); + sendMessage({ text: message.text || "Sent with attachments" }); setText(""); }; @@ -568,74 +145,87 @@ const Example = () => { }); }; - const handleSuggestionClick = (suggestion: string) => { - setStatus("submitted"); - addUserMessage(suggestion); - }; - return (
- {messages.map(({ versions, ...message }) => ( - - - {versions.map((version) => ( - -
- {message.sources?.length && ( - - - - {message.sources.map((source) => ( - - ))} - - - )} - {message.reasoning && ( - - - - {message.reasoning.content} - - - )} - {(message.from === "user" || - message.isReasoningComplete || - !message.reasoning) && ( - { + const sources = message.parts.filter( + (p) => p.type === "source-url" + ); + const reasoningParts = message.parts.filter( + (p) => p.type === "reasoning" + ); + const toolParts = message.parts.filter((p) => + p.type.startsWith("tool-") + ); + const textParts = message.parts.filter((p) => p.type === "text"); + + return ( + +
+ {sources.length > 0 && ( + + + + {sources.map((source, i) => ( + + ))} + + + )} + {toolParts.map((toolPart, i) => { + if (toolPart.type.startsWith("tool-")) { + const tool = toolPart as ToolUIPart; + return ( + - {version.content} - + + + + + + + ); + } + return null; + })} + {reasoningParts.map((reasoningPart, i) => ( + + + {reasoningPart.text} + + ))} + {textParts.map((textPart, i) => ( + - - ))} - - {versions.length > 1 && ( - - - - - - )} - - ))} + key={`${message.id}-text-${i}`} + > + {textPart.text} + + ))} +
+
+ ); + })} From 26cf7da6a74570cc9130debc882a65c942a223ab Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Tue, 4 Nov 2025 06:58:47 -0800 Subject: [PATCH 3/9] update chunking time and last message logic --- packages/examples/src/demo-chatgpt.tsx | 8 +++++--- packages/examples/src/demo-claude.tsx | 8 +++++--- packages/examples/src/demo-grok.tsx | 8 +++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/examples/src/demo-chatgpt.tsx b/packages/examples/src/demo-chatgpt.tsx index 09cd7643..d1fcc874 100644 --- a/packages/examples/src/demo-chatgpt.tsx +++ b/packages/examples/src/demo-chatgpt.tsx @@ -101,10 +101,12 @@ const Example = () => { }), initialMessages: [], onFinish: ({ messages }) => { - // When the last message is the first demo message, send the last demo message + // When finishing a message, send the next message in the list if it exists const lastUserMessageText = getLastUserMessageText(messages); - if (lastUserMessageText === userMessageTexts[0]) { - sendMessage({ text: userMessageTexts[userMessageTexts.length - 1] }); + const lastUserMessageTextIndex = userMessageTexts.indexOf(lastUserMessageText); + const nextMessageTextIndex = lastUserMessageTextIndex !== -1 ? lastUserMessageTextIndex + 1 : null; + if (nextMessageTextIndex !== null && userMessageTexts[nextMessageTextIndex]) { + sendMessage({ text: userMessageTexts[nextMessageTextIndex] }); } }, }); diff --git a/packages/examples/src/demo-claude.tsx b/packages/examples/src/demo-claude.tsx index 03fe49f9..5d2fc71c 100644 --- a/packages/examples/src/demo-claude.tsx +++ b/packages/examples/src/demo-claude.tsx @@ -110,10 +110,12 @@ const Example = () => { }), initialMessages: [], onFinish: ({ messages }) => { - // When the last message is the first demo message, send the last demo message + // When finishing a message, send the next message in the list if it exists const lastUserMessageText = getLastUserMessageText(messages); - if (lastUserMessageText === userMessageTexts[0]) { - sendMessage({ text: userMessageTexts[userMessageTexts.length - 1] }); + const lastUserMessageTextIndex = userMessageTexts.indexOf(lastUserMessageText); + const nextMessageTextIndex = lastUserMessageTextIndex !== -1 ? lastUserMessageTextIndex + 1 : null; + if (nextMessageTextIndex !== null && userMessageTexts[nextMessageTextIndex]) { + sendMessage({ text: userMessageTexts[nextMessageTextIndex] }); } }, }); diff --git a/packages/examples/src/demo-grok.tsx b/packages/examples/src/demo-grok.tsx index cf5dc748..408d5524 100644 --- a/packages/examples/src/demo-grok.tsx +++ b/packages/examples/src/demo-grok.tsx @@ -112,10 +112,12 @@ const Example = () => { }), initialMessages: [], onFinish: ({ messages }) => { - // When the last message is the first demo message, send the last demo message + // When finishing a message, send the next message in the list if it exists const lastUserMessageText = getLastUserMessageText(messages); - if (lastUserMessageText === userMessageTexts[0]) { - sendMessage({ text: userMessageTexts[userMessageTexts.length - 1] }); + const lastUserMessageTextIndex = userMessageTexts.indexOf(lastUserMessageText); + const nextMessageTextIndex = lastUserMessageTextIndex !== -1 ? lastUserMessageTextIndex + 1 : null; + if (nextMessageTextIndex !== null && userMessageTexts[nextMessageTextIndex]) { + sendMessage({ text: userMessageTexts[nextMessageTextIndex] }); } }, }); From 07331546aab3f138a3c7ae557567199140e734c1 Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Tue, 4 Nov 2025 07:29:27 -0800 Subject: [PATCH 4/9] fixe typings --- packages/examples/package.json | 4 ++-- packages/examples/src/demo-chat-data.tsx | 2 +- packages/examples/src/demo-chatgpt.tsx | 1 - packages/examples/src/demo-claude.tsx | 1 - packages/examples/src/demo-grok.tsx | 1 - pnpm-lock.yaml | 16 ++++++++-------- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/examples/package.json b/packages/examples/package.json index 8bec68b8..550004cf 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -9,10 +9,10 @@ "dependencies": { "@ai-sdk/react": "^2.0.87", "@icons-pack/react-simple-icons": "^13.8.0", - "@loremllm/transport": "^0.4.3", + "@loremllm/transport": "^0.4.5", "@repo/elements": "workspace:*", "@xyflow/react": "^12.9.0", - "ai": "5.1.0-beta.22", + "ai": "5.0.87", "lucide-react": "^0.548.0", "nanoid": "^5.1.6", "react": "19.2.0", diff --git a/packages/examples/src/demo-chat-data.tsx b/packages/examples/src/demo-chat-data.tsx index 9206bdd3..4962deb8 100644 --- a/packages/examples/src/demo-chat-data.tsx +++ b/packages/examples/src/demo-chat-data.tsx @@ -30,7 +30,7 @@ export const mockMessages = new Map([ { type: "tool-mcp", toolCallId: nanoid(), - state: "output-available" as const, + state: "output-available", input: { query: "React hooks best practices", source: "react.dev", diff --git a/packages/examples/src/demo-chatgpt.tsx b/packages/examples/src/demo-chatgpt.tsx index d1fcc874..dc8fdd75 100644 --- a/packages/examples/src/demo-chatgpt.tsx +++ b/packages/examples/src/demo-chatgpt.tsx @@ -99,7 +99,6 @@ const Example = () => { }; }, }), - initialMessages: [], onFinish: ({ messages }) => { // When finishing a message, send the next message in the list if it exists const lastUserMessageText = getLastUserMessageText(messages); diff --git a/packages/examples/src/demo-claude.tsx b/packages/examples/src/demo-claude.tsx index 5d2fc71c..39a1062b 100644 --- a/packages/examples/src/demo-claude.tsx +++ b/packages/examples/src/demo-claude.tsx @@ -108,7 +108,6 @@ const Example = () => { }; }, }), - initialMessages: [], onFinish: ({ messages }) => { // When finishing a message, send the next message in the list if it exists const lastUserMessageText = getLastUserMessageText(messages); diff --git a/packages/examples/src/demo-grok.tsx b/packages/examples/src/demo-grok.tsx index 408d5524..9369fd1f 100644 --- a/packages/examples/src/demo-grok.tsx +++ b/packages/examples/src/demo-grok.tsx @@ -110,7 +110,6 @@ const Example = () => { }; }, }), - initialMessages: [], onFinish: ({ messages }) => { // When finishing a message, send the next message in the list if it exists const lastUserMessageText = getLastUserMessageText(messages); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92897a7c..99fc4bee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -261,8 +261,8 @@ importers: specifier: ^13.8.0 version: 13.8.0(react@19.2.0) '@loremllm/transport': - specifier: ^0.4.3 - version: 0.4.3(ai@5.1.0-beta.22(zod@4.1.12)) + specifier: ^0.4.5 + version: 0.4.5(ai@5.0.87(zod@4.1.12)) '@repo/elements': specifier: workspace:* version: link:../elements @@ -270,8 +270,8 @@ importers: specifier: ^12.9.0 version: 12.9.0(@types/react@19.2.2)(immer@10.1.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) ai: - specifier: 5.1.0-beta.22 - version: 5.1.0-beta.22(zod@4.1.12) + specifier: 5.0.87 + version: 5.0.87(zod@4.1.12) lucide-react: specifier: ^0.548.0 version: 0.548.0(react@19.2.0) @@ -1154,8 +1154,8 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@loremllm/transport@0.4.3': - resolution: {integrity: sha512-YKoYfmGeFEMeE3St+U9OuhI/2wWkafYOFZlAIOGmszjBpt8474HXhvveCNdGgt1LkS2s20W6uSd0UuAmrH4Ltw==} + '@loremllm/transport@0.4.5': + resolution: {integrity: sha512-KODYFFYCd7RcAcxxSy1fbAi0PjSSKmZWbnPELvE+l/B+Dep5EGDcwb4YUSrYqfxJLiPrMV4gtvk6RrmAVnaFOw==} peerDependencies: ai: ^5.x.x @@ -6726,9 +6726,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@loremllm/transport@0.4.3(ai@5.1.0-beta.22(zod@4.1.12))': + '@loremllm/transport@0.4.5(ai@5.0.87(zod@4.1.12))': dependencies: - ai: 5.1.0-beta.22(zod@4.1.12) + ai: 5.0.87(zod@4.1.12) '@manypkg/find-root@1.1.0': dependencies: From 10fab559b04a0770b1c2944af743b4bc536bfbda Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Wed, 28 Jan 2026 20:30:04 -0800 Subject: [PATCH 5/9] refactor(examples): remove unused state and redundant type annotations - Remove unused useWebSearch/useMicrophone state in demo-chatgpt and demo-grok - Remove redundant useState and useState annotations Co-Authored-By: Claude Opus 4.5 --- packages/examples/src/demo-chatgpt.tsx | 167 +++++++++-------------- packages/examples/src/demo-claude.tsx | 181 ++++++++++--------------- packages/examples/src/demo-grok.tsx | 169 +++++++++-------------- 3 files changed, 195 insertions(+), 322 deletions(-) diff --git a/packages/examples/src/demo-chatgpt.tsx b/packages/examples/src/demo-chatgpt.tsx index 6e270a64..eebc7c25 100644 --- a/packages/examples/src/demo-chatgpt.tsx +++ b/packages/examples/src/demo-chatgpt.tsx @@ -1,7 +1,5 @@ "use client"; -import { useChat } from "@ai-sdk/react"; -import { StaticChatTransport } from "@loremllm/transport"; import { Conversation, ConversationContent, @@ -9,6 +7,12 @@ import { } from "@repo/elements/conversation"; import { Message, + MessageBranch, + MessageBranchContent, + MessageBranchNext, + MessageBranchPage, + MessageBranchPrevious, + MessageBranchSelector, MessageContent, MessageResponse, } from "@repo/elements/message"; @@ -46,7 +50,6 @@ import { DropdownMenuTrigger, } from "@repo/shadcn-ui/components/ui/dropdown-menu"; import { cn } from "@repo/shadcn-ui/lib/utils"; -import type { ToolUIPart } from "ai"; import { AudioWaveformIcon, CameraIcon, @@ -56,69 +59,16 @@ import { PaperclipIcon, ScreenShareIcon, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; -import { - getLastUserMessageText, - mockMessages, - mockResponses, - suggestions, - userMessageTexts, -} from "./demo-chat-data"; - -const Example = () => { - const [text, setText] = useState(""); - const [useWebSearch, setUseWebSearch] = useState(false); - const [useMicrophone, setUseMicrophone] = useState(false); - - const { messages, sendMessage } = useChat({ - id: "demo-chatgpt", - transport: new StaticChatTransport({ - chunkDelayMs: [20, 50], - async *mockResponse({ messages }) { - const lastUserMessageText = getLastUserMessageText(messages); - // If we already have a mock response for the user message: - const assistantParts = mockMessages.get(lastUserMessageText); - if (assistantParts) { - for (const part of assistantParts) yield part; - return; - } +import { categorizeMessageParts, getMessageVersions, suggestions } from "./demo-chat-data"; +import { useDemoChat } from "./demo-chat-shared"; - // Default response for user messages that aren't structurally defined - const randomResponse = - mockResponses[Math.floor(Math.random() * mockResponses.length)]; - - if (Math.random() > 0.5) { - yield { - type: "reasoning", - text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", - }; - } - - yield { - type: "text", - text: randomResponse, - }; - }, - }), - onFinish: ({ messages }) => { - // When finishing a message, send the next message in the list if it exists - const lastUserMessageText = getLastUserMessageText(messages); - const lastUserMessageTextIndex = userMessageTexts.indexOf(lastUserMessageText); - const nextMessageTextIndex = lastUserMessageTextIndex !== -1 ? lastUserMessageTextIndex + 1 : null; - if (nextMessageTextIndex !== null && userMessageTexts[nextMessageTextIndex]) { - sendMessage({ text: userMessageTexts[nextMessageTextIndex] }); - } - }, - }); +const Example = () => { + const [text, setText] = useState(""); - // Initialize with first user message - useEffect(() => { - if (messages.length === 0 && userMessageTexts.length > 0) { - sendMessage({ text: userMessageTexts[0] }); - } - }, [messages.length, sendMessage]); + const { messages, sendMessage } = useDemoChat("demo-chatgpt"); const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); @@ -147,16 +97,33 @@ const Example = () => { {messages.map((message) => { - const sources = message.parts.filter( - (p) => p.type === "source-url" - ); - const reasoningParts = message.parts.filter( - (p) => p.type === "reasoning" - ); - const toolParts = message.parts.filter((p) => - p.type.startsWith("tool-") - ); - const textParts = message.parts.filter((p) => p.type === "text"); + const { sources, reasoning, tools, text } = categorizeMessageParts(message.parts); + const messageText = text[0]?.text ?? ""; + + // Check if user message has alternative versions + if (message.role === "user") { + const versions = getMessageVersions(messageText); + if (versions) { + return ( + + + {versions.map((version, i) => ( + + + {version} + + + ))} + + + + + + + + ); + } + } return ( @@ -175,48 +142,42 @@ const Example = () => { )} - {toolParts.map((toolPart, i) => { - if (toolPart.type.startsWith("tool-")) { - const tool = toolPart as ToolUIPart; - return ( - - - - - - - - ); - } - return null; - })} - {reasoningParts.map((reasoningPart, i) => ( + {tools.map((tool, i) => ( + + + + + + + + ))} + {reasoning.map((part, i) => ( - {reasoningPart.text} + {part.text} ))} - {textParts.map((textPart, i) => ( + {text.map((part, i) => ( - {textPart.text} + {part.text} ))}
@@ -278,7 +239,6 @@ const Example = () => { setUseWebSearch(!useWebSearch)} variant="outline" > @@ -287,7 +247,6 @@ const Example = () => { setUseMicrophone(!useMicrophone)} variant="secondary" > diff --git a/packages/examples/src/demo-claude.tsx b/packages/examples/src/demo-claude.tsx index afd338f2..e6961cf6 100644 --- a/packages/examples/src/demo-claude.tsx +++ b/packages/examples/src/demo-claude.tsx @@ -1,7 +1,5 @@ "use client"; -import { useChat } from "@ai-sdk/react"; -import { StaticChatTransport } from "@loremllm/transport"; import { Conversation, ConversationContent, @@ -9,7 +7,12 @@ import { } from "@repo/elements/conversation"; import { Message, - MessageAvatar, + MessageBranch, + MessageBranchContent, + MessageBranchNext, + MessageBranchPage, + MessageBranchPrevious, + MessageBranchSelector, MessageContent, MessageResponse, } from "@repo/elements/message"; @@ -60,7 +63,6 @@ import { DropdownMenuTrigger, } from "@repo/shadcn-ui/components/ui/dropdown-menu"; import { cn } from "@repo/shadcn-ui/lib/utils"; -import type { ToolUIPart } from "ai"; import { ArrowUpIcon, CameraIcon, @@ -71,14 +73,11 @@ import { ScreenShareIcon, Settings2Icon, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; -import { - getLastUserMessageText, - mockMessages, - mockResponses, - userMessageTexts, -} from "./demo-chat-data"; + +import { categorizeMessageParts, getMessageVersions } from "./demo-chat-data"; +import { useDemoChat } from "./demo-chat-shared"; const models = [ { @@ -105,61 +104,13 @@ const models = [ ]; const Example = () => { - const [model, setModel] = useState(models[0].id); + const [model, setModel] = useState(models[0].id); const [modelSelectorOpen, setModelSelectorOpen] = useState(false); - const [text, setText] = useState(""); - - const { messages, sendMessage, status } = useChat({ - id: "demo-claude", - transport: new StaticChatTransport({ - chunkDelayMs: [20, 50], - async *mockResponse({ messages }) { - const lastUserMessageText = getLastUserMessageText(messages); - - // If we already have a mock response for the user message: - const assistantParts = mockMessages.get(lastUserMessageText); - if (assistantParts) { - for (const part of assistantParts) yield part; - return; - } - - // Default response for user messages that aren't structurally defined - const randomResponse = - mockResponses[Math.floor(Math.random() * mockResponses.length)]; - - if (Math.random() > 0.5) { - yield { - type: "reasoning", - text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", - }; - } - - yield { - type: "text", - text: randomResponse, - }; - }, - }), - onFinish: ({ messages }) => { - // When finishing a message, send the next message in the list if it exists - const lastUserMessageText = getLastUserMessageText(messages); - const lastUserMessageTextIndex = userMessageTexts.indexOf(lastUserMessageText); - const nextMessageTextIndex = lastUserMessageTextIndex !== -1 ? lastUserMessageTextIndex + 1 : null; - if (nextMessageTextIndex !== null && userMessageTexts[nextMessageTextIndex]) { - sendMessage({ text: userMessageTexts[nextMessageTextIndex] }); - } - }, - }); + const [text, setText] = useState(""); + const { messages, sendMessage, status } = useDemoChat("demo-claude"); const selectedModelData = models.find((m) => m.id === model); - // Initialize with first user message - useEffect(() => { - if (messages.length === 0 && userMessageTexts.length > 0) { - sendMessage({ text: userMessageTexts[0] }); - } - }, [messages.length, sendMessage]); - const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); const hasAttachments = Boolean(message.files?.length); @@ -183,16 +134,37 @@ const Example = () => { {messages.map((message) => { - const sources = message.parts.filter( - (p) => p.type === "source-url" - ); - const reasoningParts = message.parts.filter( - (p) => p.type === "reasoning" - ); - const toolParts = message.parts.filter((p) => - p.type.startsWith("tool-") - ); - const textParts = message.parts.filter((p) => p.type === "text"); + const { sources, reasoning, tools, text } = categorizeMessageParts(message.parts); + const messageText = text[0]?.text ?? ""; + + // Check if user message has alternative versions + if (message.role === "user") { + const versions = getMessageVersions(messageText); + if (versions) { + return ( + + + {versions.map((version, i) => ( + + + {version} + + + ))} + + + + + + + + ); + } + } return ( { )} - {toolParts.map((toolPart, i) => { - if (toolPart.type.startsWith("tool-")) { - const tool = toolPart as ToolUIPart; - return ( - - - - - - - - ); - } - return null; - })} - {reasoningParts.map((reasoningPart, i) => ( + {tools.map((tool, i) => ( + + + + + + + + ))} + {reasoning.map((part, i) => ( - {reasoningPart.text} + {part.text} ))} - {textParts.map((textPart, i) => ( + {text.map((part, i) => ( -
- {message.role === "user" && ( - - )} -
- {textPart.text} -
-
+ {part.text}
))}
diff --git a/packages/examples/src/demo-grok.tsx b/packages/examples/src/demo-grok.tsx index fadcb11b..503c3961 100644 --- a/packages/examples/src/demo-grok.tsx +++ b/packages/examples/src/demo-grok.tsx @@ -1,7 +1,5 @@ "use client"; -import { useChat } from "@ai-sdk/react"; -import { StaticChatTransport } from "@loremllm/transport"; import { Conversation, ConversationContent, @@ -9,6 +7,12 @@ import { } from "@repo/elements/conversation"; import { Message, + MessageBranch, + MessageBranchContent, + MessageBranchNext, + MessageBranchPage, + MessageBranchPrevious, + MessageBranchSelector, MessageContent, MessageResponse, } from "@repo/elements/message"; @@ -58,7 +62,6 @@ import { DropdownMenuTrigger, } from "@repo/shadcn-ui/components/ui/dropdown-menu"; import { cn } from "@repo/shadcn-ui/lib/utils"; -import type { ToolUIPart } from "ai"; import { AudioWaveformIcon, CameraIcon, @@ -71,14 +74,11 @@ import { ScreenShareIcon, SearchIcon, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { toast } from "sonner"; -import { - getLastUserMessageText, - mockMessages, - mockResponses, - userMessageTexts, -} from "./demo-chat-data"; + +import { categorizeMessageParts, getMessageVersions } from "./demo-chat-data"; +import { useDemoChat } from "./demo-chat-shared"; const models = [ { @@ -98,63 +98,13 @@ const models = [ ]; const Example = () => { - const [model, setModel] = useState(models[0].id); + const [model, setModel] = useState(models[0].id); const [modelSelectorOpen, setModelSelectorOpen] = useState(false); - const [text, setText] = useState(""); - const [useWebSearch, setUseWebSearch] = useState(false); - const [useMicrophone, setUseMicrophone] = useState(false); - - const { messages, sendMessage } = useChat({ - id: "demo-grok", - transport: new StaticChatTransport({ - chunkDelayMs: [20, 50], - async *mockResponse({ messages }) { - const lastUserMessageText = getLastUserMessageText(messages); - - // If we already have a mock response for the user message: - const assistantParts = mockMessages.get(lastUserMessageText); - if (assistantParts) { - for (const part of assistantParts) yield part; - return; - } - - // Default response for user messages that aren't structurally defined - const randomResponse = - mockResponses[Math.floor(Math.random() * mockResponses.length)]; - - if (Math.random() > 0.5) { - yield { - type: "reasoning", - text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", - }; - } - - yield { - type: "text", - text: randomResponse, - }; - }, - }), - onFinish: ({ messages }) => { - // When finishing a message, send the next message in the list if it exists - const lastUserMessageText = getLastUserMessageText(messages); - const lastUserMessageTextIndex = userMessageTexts.indexOf(lastUserMessageText); - const nextMessageTextIndex = lastUserMessageTextIndex !== -1 ? lastUserMessageTextIndex + 1 : null; - if (nextMessageTextIndex !== null && userMessageTexts[nextMessageTextIndex]) { - sendMessage({ text: userMessageTexts[nextMessageTextIndex] }); - } - }, - }); + const [text, setText] = useState(""); + const { messages, sendMessage } = useDemoChat("demo-grok"); const selectedModelData = models.find((m) => m.id === model); - // Initialize with first user message - useEffect(() => { - if (messages.length === 0 && userMessageTexts.length > 0) { - sendMessage({ text: userMessageTexts[0] }); - } - }, [messages.length, sendMessage]); - const handleSubmit = (message: PromptInputMessage) => { const hasText = Boolean(message.text); const hasAttachments = Boolean(message.files?.length); @@ -178,16 +128,33 @@ const Example = () => { {messages.map((message) => { - const sources = message.parts.filter( - (p) => p.type === "source-url" - ); - const reasoningParts = message.parts.filter( - (p) => p.type === "reasoning" - ); - const toolParts = message.parts.filter((p) => - p.type.startsWith("tool-") - ); - const textParts = message.parts.filter((p) => p.type === "text"); + const { sources, reasoning, tools, text } = categorizeMessageParts(message.parts); + const messageText = text[0]?.text ?? ""; + + // Check if user message has alternative versions + if (message.role === "user") { + const versions = getMessageVersions(messageText); + if (versions) { + return ( + + + {versions.map((version, i) => ( + + + {version} + + + ))} + + + + + + + + ); + } + } return ( @@ -206,48 +173,42 @@ const Example = () => { )} - {toolParts.map((toolPart, i) => { - if (toolPart.type.startsWith("tool-")) { - const tool = toolPart as ToolUIPart; - return ( - - - - - - - - ); - } - return null; - })} - {reasoningParts.map((reasoningPart, i) => ( + {tools.map((tool, i) => ( + + + + + + + + ))} + {reasoning.map((part, i) => ( - {reasoningPart.text} + {part.text} ))} - {textParts.map((textPart, i) => ( + {text.map((part, i) => ( - {textPart.text} + {part.text} ))}
@@ -310,7 +271,6 @@ const Example = () => {
setUseWebSearch(!useWebSearch)} variant="ghost" > @@ -389,7 +349,6 @@ const Example = () => { setUseMicrophone(!useMicrophone)} variant="default" > From 177c9c5995ef50e123e8e1d1b663732c882562bd Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Wed, 28 Jan 2026 20:32:02 -0800 Subject: [PATCH 6/9] use shared state --- packages/examples/package.json | 6 +- packages/examples/src/demo-chat-data.tsx | 118 +++++++++------ packages/examples/src/demo-chat-shared.tsx | 77 ++++++++++ pnpm-lock.yaml | 164 +++++++-------------- 4 files changed, 210 insertions(+), 155 deletions(-) create mode 100644 packages/examples/src/demo-chat-shared.tsx diff --git a/packages/examples/package.json b/packages/examples/package.json index f976cd0e..0fe5a819 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -7,12 +7,12 @@ "./*": "./src/*.tsx" }, "dependencies": { - "@ai-sdk/react": "^2.0.87", + "@ai-sdk/react": "^3.0.61", "@icons-pack/react-simple-icons": "^13.8.0", - "@loremllm/transport": "^0.4.5", + "@loremllm/transport": "^0.6.1", "@repo/elements": "workspace:*", "@xyflow/react": "^12.10.0", - "ai": "5.0.87", + "ai": "^6.0.39", "lucide-react": "^0.562.0", "nanoid": "^5.1.6", "react": "19.2.3", diff --git a/packages/examples/src/demo-chat-data.tsx b/packages/examples/src/demo-chat-data.tsx index 4962deb8..c74f2ebb 100644 --- a/packages/examples/src/demo-chat-data.tsx +++ b/packages/examples/src/demo-chat-data.tsx @@ -1,6 +1,16 @@ -"use client"; - -import type { UIMessage } from "ai"; +import type { + ReasoningUIPart, + SourceUrlUIPart, + TextUIPart, + ToolUIPart, + UIMessage, +} from "ai"; +import { + isReasoningUIPart, + isStaticToolUIPart, + isTextUIPart, +} from "ai"; +import type { LucideIcon } from "lucide-react"; import { BarChartIcon, BoxIcon, @@ -27,35 +37,6 @@ export const mockMessages = new Map([ url: "https://react.dev/reference/react-dom", title: "React DOM Documentation", }, - { - type: "tool-mcp", - toolCallId: nanoid(), - state: "output-available", - input: { - query: "React hooks best practices", - source: "react.dev", - }, - output: `{ - "query": "React hooks best practices", - "results": [ - { - "title": "Rules of Hooks", - "url": "https://react.dev/warnings/invalid-hook-call-warning", - "snippet": "Hooks must be called at the top level of your React function components or custom hooks. Don't call hooks inside loops, conditions, or nested functions." - }, - { - "title": "useState Hook", - "url": "https://react.dev/reference/react/useState", - "snippet": "useState is a React Hook that lets you add state to your function components. It returns an array with two values: the current state and a function to update it." - }, - { - "title": "useEffect Hook", - "url": "https://react.dev/reference/react/useEffect", - "snippet": "useEffect lets you synchronize a component with external systems. It runs after render and can be used to perform side effects like data fetching." - } - ] - }`, - }, { type: "text", text: `# React Hooks Best Practices @@ -166,9 +147,36 @@ Note that ~~class-based lifecycle methods~~ like \`componentDidMount\` are now r ], ]); -export const userMessageTexts = Array.from(mockMessages.keys()); +// Scripted user messages in order (first version only for arrays) +export const scriptedUserMessages = [ + "Can you explain how to use React hooks effectively?", + "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", +]; + +// Map of message text -> all versions (for MessageBranch UI) +const messageVersionsMap = new Map([ + [ + "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", + [ + "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", + "I'm particularly interested in understanding when to use useCallback vs useMemo. Can you provide some practical examples?", + "Thanks for the overview! Could you dive deeper into the performance optimization hooks? I want to understand the tradeoffs.", + ], + ], +]); + +// Get alternative versions for a user message text (O(1) lookup) +export function getMessageVersions(text: string): string[] | null { + return messageVersionsMap.get(text) ?? null; +} + +export type Suggestion = { + icon: LucideIcon | null; + text: string; + color?: string; +}; -export const suggestions = [ +export const suggestions: readonly Suggestion[] = [ { icon: BarChartIcon, text: "Analyze data", color: "#76d0eb" }, { icon: BoxIcon, text: "Surprise me", color: "#76d0eb" }, { icon: NotepadTextIcon, text: "Summarize text", color: "#ea8444" }, @@ -177,7 +185,7 @@ export const suggestions = [ { icon: null, text: "More" }, ]; -export const mockResponses = [ +export const mockResponses: readonly string[] = [ "That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.", "I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.", "This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...", @@ -185,10 +193,38 @@ export const mockResponses = [ "That's definitely worth exploring. From what I can see, the best way to handle this is to consider both the theoretical aspects and practical implementation details.", ]; -export const getLastUserMessageText = (messages: UIMessage[]) => { - const lastUserMessage = [...messages] - .reverse() - .find((msg) => msg.role === "user"); - const textPart = lastUserMessage?.parts.find((p) => p.type === "text"); - return textPart && "text" in textPart ? textPart.text : ""; +export function getLastUserMessageText(messages: UIMessage[]): string { + const lastUserMessage = messages.findLast((msg) => msg.role === "user"); + const textPart = lastUserMessage?.parts.find(isTextUIPart); + return textPart?.text ?? ""; +} + +// Categorized message parts with proper types +export type CategorizedParts = { + sources: SourceUrlUIPart[]; + reasoning: ReasoningUIPart[]; + tools: ToolUIPart[]; + text: TextUIPart[]; }; + +// Single-pass categorization of message parts using SDK type guards +export function categorizeMessageParts(parts: UIMessage["parts"]): CategorizedParts { + const sources: SourceUrlUIPart[] = []; + const reasoning: ReasoningUIPart[] = []; + const tools: ToolUIPart[] = []; + const text: TextUIPart[] = []; + + for (const p of parts) { + if (isTextUIPart(p)) { + text.push(p); + } else if (isReasoningUIPart(p)) { + reasoning.push(p); + } else if (isStaticToolUIPart(p)) { + tools.push(p); + } else if (p.type === "source-url") { + sources.push(p); + } + } + + return { sources, reasoning, tools, text }; +} diff --git a/packages/examples/src/demo-chat-shared.tsx b/packages/examples/src/demo-chat-shared.tsx new file mode 100644 index 00000000..0fe6f6a8 --- /dev/null +++ b/packages/examples/src/demo-chat-shared.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; +import { StaticChatTransport } from "@loremllm/transport"; +import { useEffect, useRef } from "react"; + +import { + getLastUserMessageText, + mockMessages, + mockResponses, + scriptedUserMessages, +} from "./demo-chat-data"; + +// Shared transport for all demos +export const demoTransport = new StaticChatTransport({ + chunkDelayMs: [20, 50], + async *mockResponse({ messages }) { + const lastUserMessageText = getLastUserMessageText(messages); + + // Check for predefined response + const assistantParts = mockMessages.get(lastUserMessageText); + if (assistantParts) { + for (const part of assistantParts) yield part; + return; + } + + // Fallback to random response + const randomResponse = + mockResponses[Math.floor(Math.random() * mockResponses.length)]; + + if (Math.random() > 0.5) { + yield { + type: "reasoning", + text: "Let me think about this question carefully. I need to provide a comprehensive and helpful response that addresses the user's needs while being clear and concise.", + }; + } + + yield { + type: "text", + text: randomResponse, + }; + }, +}); + +// Hook for demo chat with auto-initialization and scripted flow +export function useDemoChat(chatId: string) { + const { messages, sendMessage, setMessages, status } = useChat({ + id: chatId, + transport: demoTransport, + onFinish: ({ messages }) => { + // Auto-send next scripted message + const lastText = getLastUserMessageText(messages); + const nextIndex = scriptedUserMessages.indexOf(lastText) + 1; + + if (nextIndex < scriptedUserMessages.length) { + sendMessage({ text: scriptedUserMessages[nextIndex] }); + } + }, + }); + + // Track initialization to avoid double-firing in strict mode + const initialized = useRef(false); + + useEffect(() => { + if (initialized.current) return; + initialized.current = true; + + demoTransport.clearCache(chatId); + setMessages([]); + if (scriptedUserMessages.length > 0) { + sendMessage({ text: scriptedUserMessages[0] }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { messages, sendMessage, status }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5d7fbed..9d0db9a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,14 +301,14 @@ importers: packages/examples: dependencies: '@ai-sdk/react': - specifier: ^2.0.87 - version: 2.0.125(react@19.2.3)(zod@4.3.5) + specifier: ^3.0.61 + version: 3.0.61(react@19.2.3)(zod@4.3.5) '@icons-pack/react-simple-icons': specifier: ^13.8.0 version: 13.8.0(react@19.2.3) '@loremllm/transport': - specifier: ^0.4.5 - version: 0.4.6(ai@5.0.87(zod@4.3.5)) + specifier: ^0.6.1 + version: 0.6.1(ai@6.0.39(zod@4.3.5)) '@repo/elements': specifier: workspace:* version: link:../elements @@ -316,8 +316,8 @@ importers: specifier: ^12.10.0 version: 12.10.0(@types/react@19.2.8)(immer@10.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) ai: - specifier: 5.0.87 - version: 5.0.87(zod@4.3.5) + specifier: ^6.0.39 + version: 6.0.39(zod@4.3.5) lucide-react: specifier: ^0.562.0 version: 0.562.0(react@19.2.3) @@ -449,32 +449,20 @@ packages: '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - '@ai-sdk/gateway@2.0.29': - resolution: {integrity: sha512-1b7E9F/B5gex/1uCkhs+sGIbH0KsZOItHnNz3iY5ir+nc4ZUA6WOU5Cu2w1USlc+3UVbhf+H+iNLlxVjLe4VvQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/gateway@2.0.6': - resolution: {integrity: sha512-FmhR6Tle09I/RUda8WSPpJ57mjPWzhiVVlB50D+k+Qf/PBW0CBtnbAUxlNSR5v+NIZNLTK3C56lhb23ntEdxhQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.16': resolution: {integrity: sha512-OOY5CfRJiHvh/8np2vs1RQaCZ5hWv2qOeEmmeiABXK3gLQHUVnCO+1hhoLsZdHM5iElu6M407dAOfyvTsKJqcQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.16': - resolution: {integrity: sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA==} + '@ai-sdk/gateway@3.0.27': + resolution: {integrity: sha512-Pr+ApS9k6/jcR3kNltJNxo60OdYvnVU4DeRhzVtxUAYTXCHx4qO+qTMG9nNRn+El1acJnNRA//Su47srjXkT/w==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@3.0.20': - resolution: {integrity: sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ==} + '@ai-sdk/provider-utils@4.0.10': + resolution: {integrity: sha512-VeDAiCH+ZK8Xs4hb9Cw7pHlujWNL52RKe8TExOkrw6Ir1AmfajBZTb9XUdKOZO08RwQElIKA8+Ltm+Gqfo8djQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -485,27 +473,13 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@2.0.0': - resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} - engines: {node: '>=18'} - - '@ai-sdk/provider@2.0.1': - resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==} - engines: {node: '>=18'} - '@ai-sdk/provider@3.0.4': resolution: {integrity: sha512-5KXyBOSEX+l67elrEa+wqo/LSsSTtrPj9Uoh3zMbe/ceQX4ucHI3b9nUEfNkGF3Ry1svv90widAt+aiKdIJasQ==} engines: {node: '>=18'} - '@ai-sdk/react@2.0.125': - resolution: {integrity: sha512-Bew93kJxqw0ylQj8AvFZ15vZjT2abKv5NyGhWY3f0mENHgNJYe5tt8x0dsx5Btq0Yqijz5gN0MCeKru00VQzrQ==} + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} engines: {node: '>=18'} - peerDependencies: - react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 - zod: ^3.25.76 || ^4.1.8 - peerDependenciesMeta: - zod: - optional: true '@ai-sdk/react@3.0.41': resolution: {integrity: sha512-mTyfkM+WVUIlaqIpOPSgHlKL1sRQ9df+hdhs7EmDi804m/Ouw+Cq++HCJ9GFAnpnLizcO8huBoTNIrgdjewjzQ==} @@ -513,6 +487,12 @@ packages: peerDependencies: react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@ai-sdk/react@3.0.61': + resolution: {integrity: sha512-vCjZBnY2+TawFBXamSKt6elAt9n1MXMfcjSd9DSgT9peCJN27qNGVSXgaGNh/B3cUgeOktFfhB2GVmIqOjvmLQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1397,10 +1377,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@loremllm/transport@0.4.6': - resolution: {integrity: sha512-mXsMFjFnE9c9c2la+jZ3vNqQjYuyDDOZGSeTsfMIhglYomKTjtJEklIEJsC/M1w7OX+DMQY3W8z9Lsl3Os/BKQ==} + '@loremllm/transport@0.6.1': + resolution: {integrity: sha512-ox2IX9sa5rmCE5WrUCKAI6qWmlHkL7GHuj2seSIqvkdt5tRLtgGFRWVz99gv4sdqGmfqNrVNJI+ZRbNWjf/KTA==} peerDependencies: - ai: ^5.x.x + ai: ^6.x.x '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2921,10 +2901,6 @@ packages: vite: optional: true - '@vercel/oidc@3.0.3': - resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==} - engines: {node: '>= 20'} - '@vercel/oidc@3.1.0': resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} engines: {node: '>= 20'} @@ -3042,20 +3018,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ai@5.0.123: - resolution: {integrity: sha512-V3Imb0tg0pHCa6a/VsoW/FZpT07mwUw/4Hj6nexJC1Nvf1eyKQJyaYVkl+YTLnA8cKQSUkoarKhXWbFy4CSgjw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - ai@5.0.87: - resolution: {integrity: sha512-9Cjx7o8IY9zAczigX0Tk/BaQwjPe/M6DpEjejKSBNrf8mOPIvyM+pJLqJSC10IsKci3FPsnaizJeJhoetU1Wfw==} + ai@6.0.39: + resolution: {integrity: sha512-hF05gF4H+IxuilA8kNANVVHQXduTJsJaH74jmlmy8mcQt3NZgPYe2zZNyGBV4DPDYTUDt1h31hbLgQqJTn5LGA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - ai@6.0.39: - resolution: {integrity: sha512-hF05gF4H+IxuilA8kNANVVHQXduTJsJaH74jmlmy8mcQt3NZgPYe2zZNyGBV4DPDYTUDt1h31hbLgQqJTn5LGA==} + ai@6.0.59: + resolution: {integrity: sha512-9SfCvcr4kVk4t8ZzIuyHpuL1hFYKsYMQfBSbBq3dipXPa+MphARvI8wHEjNaRqYl3JOsJbWxEBIMqHL0L92mUA==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -6485,20 +6455,6 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@ai-sdk/gateway@2.0.29(zod@4.3.5)': - dependencies: - '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.20(zod@4.3.5) - '@vercel/oidc': 3.1.0 - zod: 4.3.5 - - '@ai-sdk/gateway@2.0.6(zod@4.3.5)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.16(zod@4.3.5) - '@vercel/oidc': 3.0.3 - zod: 4.3.5 - '@ai-sdk/gateway@3.0.16(zod@3.25.76)': dependencies: '@ai-sdk/provider': 3.0.4 @@ -6513,16 +6469,16 @@ snapshots: '@vercel/oidc': 3.1.0 zod: 4.3.5 - '@ai-sdk/provider-utils@3.0.16(zod@4.3.5)': + '@ai-sdk/gateway@3.0.27(zod@4.3.5)': dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.10(zod@4.3.5) + '@vercel/oidc': 3.1.0 zod: 4.3.5 - '@ai-sdk/provider-utils@3.0.20(zod@4.3.5)': + '@ai-sdk/provider-utils@4.0.10(zod@4.3.5)': dependencies: - '@ai-sdk/provider': 2.0.1 + '@ai-sdk/provider': 3.0.5 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 zod: 4.3.5 @@ -6541,27 +6497,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.5 - '@ai-sdk/provider@2.0.0': - dependencies: - json-schema: 0.4.0 - - '@ai-sdk/provider@2.0.1': - dependencies: - json-schema: 0.4.0 - '@ai-sdk/provider@3.0.4': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.125(react@19.2.3)(zod@4.3.5)': + '@ai-sdk/provider@3.0.5': dependencies: - '@ai-sdk/provider-utils': 3.0.20(zod@4.3.5) - ai: 5.0.123(zod@4.3.5) - react: 19.2.3 - swr: 2.3.8(react@19.2.3) - throttleit: 2.1.0 - optionalDependencies: - zod: 4.3.5 + json-schema: 0.4.0 '@ai-sdk/react@3.0.41(react@19.2.3)(zod@3.25.76)': dependencies: @@ -6583,6 +6525,16 @@ snapshots: transitivePeerDependencies: - zod + '@ai-sdk/react@3.0.61(react@19.2.3)(zod@4.3.5)': + dependencies: + '@ai-sdk/provider-utils': 4.0.10(zod@4.3.5) + ai: 6.0.59(zod@4.3.5) + react: 19.2.3 + swr: 2.3.8(react@19.2.3) + throttleit: 2.1.0 + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@antfu/install-pkg@1.1.0': @@ -7421,9 +7373,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@loremllm/transport@0.4.6(ai@5.0.87(zod@4.3.5))': + '@loremllm/transport@0.6.1(ai@6.0.39(zod@4.3.5))': dependencies: - ai: 5.0.87(zod@4.3.5) + ai: 6.0.39(zod@4.3.5) '@manypkg/find-root@1.1.0': dependencies: @@ -9053,8 +9005,6 @@ snapshots: transitivePeerDependencies: - debug - '@vercel/oidc@3.0.3': {} - '@vercel/oidc@3.1.0': {} '@vercel/speed-insights@1.3.1(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': @@ -9213,22 +9163,6 @@ snapshots: agent-base@7.1.4: {} - ai@5.0.123(zod@4.3.5): - dependencies: - '@ai-sdk/gateway': 2.0.29(zod@4.3.5) - '@ai-sdk/provider': 2.0.1 - '@ai-sdk/provider-utils': 3.0.20(zod@4.3.5) - '@opentelemetry/api': 1.9.0 - zod: 4.3.5 - - ai@5.0.87(zod@4.3.5): - dependencies: - '@ai-sdk/gateway': 2.0.6(zod@4.3.5) - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.16(zod@4.3.5) - '@opentelemetry/api': 1.9.0 - zod: 4.3.5 - ai@6.0.39(zod@3.25.76): dependencies: '@ai-sdk/gateway': 3.0.16(zod@3.25.76) @@ -9245,6 +9179,14 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 4.3.5 + ai@6.0.59(zod@4.3.5): + dependencies: + '@ai-sdk/gateway': 3.0.27(zod@4.3.5) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.10(zod@4.3.5) + '@opentelemetry/api': 1.9.0 + zod: 4.3.5 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 From e3a4d09481ec6fc7109330f048b277d59dfa7508 Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Wed, 28 Jan 2026 20:40:23 -0800 Subject: [PATCH 7/9] fix: prevent suggestion clicks from restarting scripted demo indexOf returns -1 for non-scripted messages, making nextIndex=0 and incorrectly triggering scriptedUserMessages[0] Co-Authored-By: Claude Opus 4.5 --- packages/examples/src/demo-chat-shared.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/examples/src/demo-chat-shared.tsx b/packages/examples/src/demo-chat-shared.tsx index 0fe6f6a8..e58b30bf 100644 --- a/packages/examples/src/demo-chat-shared.tsx +++ b/packages/examples/src/demo-chat-shared.tsx @@ -48,10 +48,14 @@ export function useDemoChat(chatId: string) { id: chatId, transport: demoTransport, onFinish: ({ messages }) => { - // Auto-send next scripted message + // Auto-send next scripted message only if current was part of script const lastText = getLastUserMessageText(messages); - const nextIndex = scriptedUserMessages.indexOf(lastText) + 1; + const currentIndex = scriptedUserMessages.indexOf(lastText); + // Don't auto-continue for non-scripted messages (e.g., suggestions) + if (currentIndex === -1) return; + + const nextIndex = currentIndex + 1; if (nextIndex < scriptedUserMessages.length) { sendMessage({ text: scriptedUserMessages[nextIndex] }); } From 023bc1deb94958316aacf1055d0e5010ceb622b4 Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Wed, 28 Jan 2026 20:47:15 -0800 Subject: [PATCH 8/9] refactor: derive scriptedUserMessages from mockMessages keys Co-Authored-By: Claude Opus 4.5 --- packages/examples/src/demo-chat-data.tsx | 27 +++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/examples/src/demo-chat-data.tsx b/packages/examples/src/demo-chat-data.tsx index c74f2ebb..4df38572 100644 --- a/packages/examples/src/demo-chat-data.tsx +++ b/packages/examples/src/demo-chat-data.tsx @@ -5,11 +5,7 @@ import type { ToolUIPart, UIMessage, } from "ai"; -import { - isReasoningUIPart, - isStaticToolUIPart, - isTextUIPart, -} from "ai"; +import { isReasoningUIPart, isStaticToolUIPart, isTextUIPart } from "ai"; import type { LucideIcon } from "lucide-react"; import { BarChartIcon, @@ -62,14 +58,14 @@ React hooks are a powerful feature that let you use state and other React featur \`\`\`jsx function ProfilePage({ userId }) { const [user, setUser] = useState(null); - + useEffect(() => { // This runs after render and when userId changes fetchUser(userId).then(userData => { setUser(userData); }); }, [userId]); - + return user ? : ; } \`\`\` @@ -84,7 +80,7 @@ Would you like me to explain any specific hook in more detail?`, { type: "reasoning", text: `The user is asking for a detailed explanation of useCallback and useMemo. I should provide a clear and concise explanation of each hook's purpose and how they differ. - + The useCallback hook is used to memoize functions to prevent unnecessary re-renders of child components that receive functions as props. The useMemo hook is used to memoize values to avoid expensive recalculations on every render. @@ -147,11 +143,10 @@ Note that ~~class-based lifecycle methods~~ like \`componentDidMount\` are now r ], ]); -// Scripted user messages in order (first version only for arrays) -export const scriptedUserMessages = [ - "Can you explain how to use React hooks effectively?", - "Yes, could you explain useCallback and useMemo in more detail? When should I use one over the other?", -]; +// Ordered sequence of user messages for auto-play demo. +// useDemoChat sends the first message on mount, then auto-sends +// each subsequent message after the previous response completes. +export const scriptedUserMessages = [...mockMessages.keys()]; // Map of message text -> all versions (for MessageBranch UI) const messageVersionsMap = new Map([ @@ -165,7 +160,7 @@ const messageVersionsMap = new Map([ ], ]); -// Get alternative versions for a user message text (O(1) lookup) +// Get alternative versions for a user message text export function getMessageVersions(text: string): string[] | null { return messageVersionsMap.get(text) ?? null; } @@ -208,7 +203,9 @@ export type CategorizedParts = { }; // Single-pass categorization of message parts using SDK type guards -export function categorizeMessageParts(parts: UIMessage["parts"]): CategorizedParts { +export function categorizeMessageParts( + parts: UIMessage["parts"], +): CategorizedParts { const sources: SourceUrlUIPart[] = []; const reasoning: ReasoningUIPart[] = []; const tools: ToolUIPart[] = []; From 379d5f6725bba0e1e10286c156ccc29a38ef50ee Mon Sep 17 00:00:00 2001 From: Kaiyu Hsu Date: Wed, 28 Jan 2026 20:49:04 -0800 Subject: [PATCH 9/9] comments --- packages/examples/src/demo-chat-data.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/src/demo-chat-data.tsx b/packages/examples/src/demo-chat-data.tsx index 4df38572..b1c403ac 100644 --- a/packages/examples/src/demo-chat-data.tsx +++ b/packages/examples/src/demo-chat-data.tsx @@ -202,7 +202,7 @@ export type CategorizedParts = { text: TextUIPart[]; }; -// Single-pass categorization of message parts using SDK type guards +// Categorization of message parts. We need this because of branching messages. export function categorizeMessageParts( parts: UIMessage["parts"], ): CategorizedParts {