From 87ad6094dc80a5355d127380c4877cacda3cbb49 Mon Sep 17 00:00:00 2001 From: gabrielraeder Date: Tue, 21 Oct 2025 19:20:53 -0300 Subject: [PATCH 1/3] test deployment --- ai-codegen.mdx | 5 +- snippets/chat-example/ChatContainer.jsx | 702 ++++++++++++++++++++++++ snippets/chat-example/ChatInterface.jsx | 321 +++++++++++ snippets/chat-example/useChat.jsx | 273 +++++++++ 4 files changed, 1298 insertions(+), 3 deletions(-) create mode 100644 snippets/chat-example/ChatContainer.jsx create mode 100644 snippets/chat-example/ChatInterface.jsx create mode 100644 snippets/chat-example/useChat.jsx diff --git a/ai-codegen.mdx b/ai-codegen.mdx index 5052f00..9640beb 100644 --- a/ai-codegen.mdx +++ b/ai-codegen.mdx @@ -1,9 +1,8 @@ --- title: Start building with AI description: Collaborate with AI to generate code or learn how to build with Circle. This model is trained on developer and user-controlled Circle wallets, SCP, CCTP (v1/v2), Gateway, and MSCA products. -mode: wide --- -import {ChatExample} from "/snippets/ChatExample.jsx"; +import {ChatContainer} from '/snippets/chat-example/ChatContainer.jsx'; - \ No newline at end of file + \ No newline at end of file diff --git a/snippets/chat-example/ChatContainer.jsx b/snippets/chat-example/ChatContainer.jsx new file mode 100644 index 0000000..a233c36 --- /dev/null +++ b/snippets/chat-example/ChatContainer.jsx @@ -0,0 +1,702 @@ +// Types +export const StreamingState = { + IDLE: "idle", + STREAMING: "streaming", + RETRYING: "retrying", +}; + +// Mock data for testing +export const MOCK_RESPONSES = [ + "Okay, I will help you with your request. Could you please specify what you want to do? Do you want to generate code?", + "I understand. Let me break this down for you step by step.", + "That's a great question! Here are some key points to consider.", + "Based on what you've told me, I recommend the following approach.", + "I can definitely help with that. Let me provide you with more details.", +]; + +// Action types +export const ACTIONS = { + ADD_USER_MESSAGE: "ADD_USER_MESSAGE", + ADD_BOT_MESSAGE: "ADD_BOT_MESSAGE", + APPEND_TO_BOT_MESSAGE: "APPEND_TO_BOT_MESSAGE", + COMPLETE_BOT_MESSAGE: "COMPLETE_BOT_MESSAGE", + SET_STREAMING_STATE: "SET_STREAMING_STATE", + SET_ERROR: "SET_ERROR", + REMOVE_LAST_BOT_MESSAGE: "REMOVE_LAST_BOT_MESSAGE", +}; + +// API endpoint +export const CHAT_API_URL = "/api/chat"; + +// Initial state +export const initialState = { + messages: [], + streamingState: StreamingState.IDLE, + error: false, +}; + +// Reducer +export const chatReducer = (state, action) => { + switch (action.type) { + case ACTIONS.ADD_USER_MESSAGE: + return { + ...state, + messages: [ + ...state.messages, + { + id: crypto.randomUUID(), + type: "user", + content: action.payload.content, + timestamp: new Date(), + }, + ], + }; + + case ACTIONS.ADD_BOT_MESSAGE: + return { + ...state, + messages: [ + ...state.messages, + { + id: action.payload.id, + type: "bot", + content: "", + timestamp: new Date(), + isComplete: false, + }, + ], + }; + + case ACTIONS.APPEND_TO_BOT_MESSAGE: + return { + ...state, + messages: state.messages.map((msg, index) => + index === state.messages.length - 1 && msg.type === "bot" + ? { ...msg, content: msg.content + action.payload.content } + : msg + ), + }; + + case ACTIONS.COMPLETE_BOT_MESSAGE: + return { + ...state, + messages: state.messages.map((msg, index) => + index === state.messages.length - 1 && msg.type === "bot" ? { ...msg, isComplete: true } : msg + ), + }; + + case ACTIONS.SET_STREAMING_STATE: + return { + ...state, + streamingState: action.payload, + }; + + case ACTIONS.SET_ERROR: + return { + ...state, + error: action.payload, + }; + + case ACTIONS.REMOVE_LAST_BOT_MESSAGE: + return { + ...state, + messages: state.messages.slice(0, -1), + }; + + default: + return state; + } +}; + +/** + * A React hook that manages chat message interactions with a streaming API endpoint. + * Provides functionality to send messages, receive streaming responses, and manage chat state. + * Falls back to mock data if API is not available. + */ +export const useChat = ({ sessionId, apiUrl = null }) => { + const [state, dispatch] = useReducer(chatReducer, initialState); + const isMounted = useRef(true); + + // Cleanup on unmount + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + // Safe dispatch to avoid updating unmounted component + const safeDispatch = useCallback( + (action) => { + if (isMounted.current) { + dispatch(action); + } + }, + [dispatch] + ); + + const handleError = useCallback(() => { + safeDispatch({ type: ACTIONS.SET_ERROR, payload: true }); + safeDispatch({ type: ACTIONS.SET_STREAMING_STATE, payload: StreamingState.IDLE }); + }, [safeDispatch]); + + // Mock streaming response + const processMockStream = useCallback( + async (content) => { + try { + // Simulate streaming by sending chunks + const words = content.split(" "); + for (let i = 0; i < words.length; i++) { + if (!isMounted.current) break; + + await new Promise((resolve) => setTimeout(resolve, 50)); + safeDispatch({ + type: ACTIONS.APPEND_TO_BOT_MESSAGE, + payload: { content: words[i] + (i < words.length - 1 ? " " : "") }, + }); + } + safeDispatch({ type: ACTIONS.COMPLETE_BOT_MESSAGE }); + } catch (error) { + handleError(); + } + }, + [safeDispatch, handleError] + ); + + // Real streaming from API + const processStream = useCallback( + async (reader) => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // Convert the chunk to text + const chunk = new TextDecoder().decode(value); + const data = JSON.parse(chunk); + + // Handle the chunk + if (data.type === "completion") { + safeDispatch({ type: ACTIONS.COMPLETE_BOT_MESSAGE }); + continue; + } + + if (data.content) { + safeDispatch({ + type: ACTIONS.APPEND_TO_BOT_MESSAGE, + payload: { content: data.content }, + }); + } + } + } catch (error) { + handleError(); + } + }, + [safeDispatch, handleError] + ); + + const handleSendMessage = useCallback( + async (content) => { + if (!isMounted.current) return; + + // Reset error state + safeDispatch({ type: ACTIONS.SET_ERROR, payload: false }); + + // Set streaming state + safeDispatch({ + type: ACTIONS.SET_STREAMING_STATE, + payload: StreamingState.STREAMING, + }); + + // Add user message + safeDispatch({ type: ACTIONS.ADD_USER_MESSAGE, payload: { content } }); + + // Add bot message placeholder + const botMessageId = crypto.randomUUID(); + safeDispatch({ type: ACTIONS.ADD_BOT_MESSAGE, payload: { id: botMessageId } }); + + try { + const endpoint = apiUrl || CHAT_API_URL; + + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: content, + sessionId, + }), + }); + + if (!response.ok) { + throw new Error("HTTP error"); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error("No reader available"); + } + + await processStream(reader); + } catch (error) { + // Fallback to mock data + console.log("Using mock response:", error); + const mockResponse = MOCK_RESPONSES[Math.floor(Math.random() * MOCK_RESPONSES.length)]; + await processMockStream(mockResponse); + } finally { + safeDispatch({ + type: ACTIONS.SET_STREAMING_STATE, + payload: StreamingState.IDLE, + }); + } + }, + [safeDispatch, sessionId, processStream, processMockStream, apiUrl] + ); + + const handleRetry = useCallback(async () => { + safeDispatch({ type: ACTIONS.REMOVE_LAST_BOT_MESSAGE }); + const lastUserMessage = state.messages.filter((message) => message.type === "user").at(-1)?.content; + + if (lastUserMessage) { + await handleSendMessage(lastUserMessage); + } + }, [safeDispatch, handleSendMessage, state.messages]); + + return { + handleSendMessage, + messages: state.messages, + streamingState: state.streamingState, + error: state.error, + handleRetry, + }; +}; + +export const CalendarIcon = () => ( + + + +); + +export const SendIcon = () => ( + + + + + + + + + + +); + +export const CircleLogoIcon = () => ( + + + + + + + + + + + + + + +); + +export const ArrowDownIcon = () => ( + + + +); + +/** + * ChatInterface Component + * Renders a chat interface with streaming responses and auto-scroll functionality. + * Manages the chat UI for a given conversation session with Tailwind CSS and dark mode support. + */ +export const ChatInterface = ({ sessionId, darkMode = false, apiUrl = null }) => { + const { handleSendMessage, messages, streamingState, error, handleRetry } = useChat({ + sessionId, + apiUrl, + }); + + const hasMessages = useMemo(() => messages.length > 0, [messages]); + + const scrollContainerRef = useRef(null); + const chatEndRef = useRef(null); + const isAtBottomRef = useRef(true); + const inputRef = useRef(null); + + const scrollToBottom = useCallback(() => { + chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); + isAtBottomRef.current = true; + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages, scrollToBottom]); + + // Handle scroll to detect if at bottom + const handleScroll = useCallback(() => { + if (!scrollContainerRef.current) return; + const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; + isAtBottomRef.current = scrollHeight - scrollTop - clientHeight < 50; + }, []); + + const handleScrollToBottom = useCallback(() => { + scrollToBottom(); + }, [scrollToBottom]); + + const handleSubmit = useCallback( + (message) => { + if (!message.trim() || streamingState !== StreamingState.IDLE) return; + void (async () => { + scrollToBottom(); + await handleSendMessage(message); + })(); + }, + [handleSendMessage, scrollToBottom, streamingState] + ); + + const handleKeyDown = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e.currentTarget.value); + e.currentTarget.value = ""; + } + }; + + const isDark = darkMode; + + return ( + <> + {/* Messages Area */} +
+
+ {!hasMessages && ( +
+
+

+ How can we help you? +

+
+
+ )} + + {hasMessages && ( +
+ {/* Date Separator */} +
+
+
+ + + Today{" "} + {new Date().toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} + +
+
+
+ + {/* Messages List */} +
    + {messages.map((message) => + message.type === "user" ? ( +
  1. +
    +

    {message.content}

    +
    +
  2. + ) : ( +
  3. + +
    + + Circle AI + + + {message.timestamp.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + })} + +
    +
    +

    {message.content}

    +
    + {error && message.id === messages.at(-1)?.id && ( +
    + +
    + )} +
  4. + ) + )} +
+ + {/* Scroll anchor */} + + )} +
+
+ + {/* Scroll to Bottom Button */} + {!isAtBottomRef.current && hasMessages && ( +
+ +
+ )} + + {/* Input Area */} +
+
+
+
+
{ + e.preventDefault(); + handleSubmit(inputRef.current?.value || ""); + if (inputRef.current) inputRef.current.value = ""; + }} + > +
+