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..00b77ea --- /dev/null +++ b/snippets/chat-example/ChatContainer.jsx @@ -0,0 +1,110 @@ +import { ChatInterface } from "/snippets/chat-example/ChatInterface.jsx"; + +/** + * Error component to display when session creation fails. + */ +export const ErrorDisplay = ({ error, isDark }) => { + const errorMessages = { + SESSION_CREATION_FAILED: "Failed to create chat session. Please try again.", + UNEXPECTED_ERROR: "An unexpected error occurred. Please try again later.", + }; + + return ( +
+ {errorMessages[error] || "An error occurred"} +
+ ); +}; + +/** + * Container component that handles session creation and renders the chat interface. + * This component creates a new chat session before rendering the chat UI. + * Supports both real API and mock session for development. + */ +export const ChatContainer = ({ darkMode = false, sessionApiUrl = null, chatApiUrl = null }) => { + const [sessionId, setSessionId] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const createSession = async () => { + try { + setIsLoading(true); + setError(null); + + // Use provided API URL or default + const apiUrl = sessionApiUrl || "/api/chat/session"; + + const response = await fetch(apiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to create session"); + } + + const data = await response.json(); + + if (!data?.sessionId) { + throw new Error("SESSION_CREATION_FAILED"); + } + + setSessionId(data.sessionId); + } catch (err) { + console.error("Error creating chat session:", err); + + // Fallback to mock session for development + console.log("Using mock session for development"); + setSessionId(`mock-session-${Date.now()}`); + + // Optionally show error only if not using mock + if (sessionApiUrl) { + setError( + err instanceof Error && err.message === "SESSION_CREATION_FAILED" + ? "SESSION_CREATION_FAILED" + : "UNEXPECTED_ERROR" + ); + } + } finally { + setIsLoading(false); + } + }; + + void createSession(); + }, [sessionApiUrl]); + + const isDark = darkMode; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error) { + return ; + } + + if (!sessionId) { + return null; + } + + return ( +
+ +
+ ); +}; diff --git a/snippets/chat-example/ChatInterface.jsx b/snippets/chat-example/ChatInterface.jsx new file mode 100644 index 0000000..2ea003e --- /dev/null +++ b/snippets/chat-example/ChatInterface.jsx @@ -0,0 +1,321 @@ +import { useChat, StreamingState } from "/snippets/chat-example/useChat.jsx"; + +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 = ""; + }} + > +
+