UNPKG

@aptos-labs/ai-chatbot-client

Version:

Aptos AI Developer Assistant - A React-based chatbot interface for the Aptos Developer Assistant API

660 lines (656 loc) 20 kB
import { createContext, useRef, useState, useCallback, useEffect, useContext } from 'react'; import { v4 } from 'uuid'; import { jsx } from 'react/jsx-runtime'; // src/types.ts var RagProvider = /* @__PURE__ */ ((RagProvider2) => { RagProvider2["DEVELOPER_DOCS"] = "developer-docs"; RagProvider2["APTOS_LEARN"] = "aptos-learn"; return RagProvider2; })(RagProvider || {}); // src/client/index.ts var ChatbotClient = class { constructor(config) { this.config = config; } // Chat Management async createChat() { const clientId = this.config.clientId; if (!clientId) { throw new Error("Client ID not found. Please ensure it is set in config."); } return { id: null, title: "New Chat", timestamp: (/* @__PURE__ */ new Date()).toISOString(), messages: [], metadata: { client_id: clientId, rag_provider: this.config.ragProvider } }; } async getChat(chatId) { const response = await this.fetch(`${this.config.apiUrl}/api/chat/history/${chatId}`); return response.json(); } async listChats() { const clientId = this.config.clientId; if (!clientId) { throw new Error("Client ID not found. Please ensure it is set in config."); } const url = `${this.config.apiUrl}/api/chat/histories?client_id=${encodeURIComponent(clientId)}`; const response = await this.fetch(url); const data = await response.json(); if (!data || !data.histories) { throw new Error("Invalid response format from server"); } return data.histories; } async deleteChat(chatId) { await this.fetch(`${this.config.apiUrl}/api/chat/history/${chatId}`, { method: "DELETE" }); } async updateChatTitle(chatId, title) { await this.fetch(`${this.config.apiUrl}/api/chat/history/${chatId}`, { method: "PATCH", body: JSON.stringify({ title }) }); } // Message Operations async sendMessage(chatId, content, options) { const clientId = this.config.clientId; if (!clientId) { throw new Error("Client ID not found. Please ensure it is set in config."); } const request = { content, client_id: clientId, role: "user", temperature: 0.7, rag_provider: this.config.ragProvider || "developer-docs", use_multi_step: !this.config.fastMode, ...options?.messageId && { message_id: options.messageId }, ...chatId && { chat_id: chatId }, ...options?.sharedChatId && { shared_chat_id: options.sharedChatId } }; const response = await this.fetch(`${this.config.apiUrl}/api/message/stream`, { method: "POST", body: JSON.stringify(request), headers: { Accept: "text/plain", // Accept plain text for streaming "Content-Type": "application/json" }, signal: options?.signal }); if (!response.ok) { const errorText = await response.text(); throw new Error( `API request failed: ${response.status} ${response.statusText} ${errorText}` ); } return response; } async getMessages(chatId, before) { const url = new URL(`${this.config.apiUrl}/api/chat/history/${chatId}`); if (before) { url.searchParams.set("before", before); } const response = await this.fetch(url.toString()); return response.json(); } async provideFeedback(messageId, feedback) { await this.fetch(`${this.config.apiUrl}/api/feedback`, { method: "POST", body: JSON.stringify({ message_id: messageId, feedback, client_id: this.config.clientId }) }); } async deleteFeedback(messageId) { await this.fetch( `${this.config.apiUrl}/api/feedback/${messageId}?client_id=${this.config.clientId}`, { method: "DELETE" } ); } // Shared Chat Operations async shareChat(chatId, options) { const clientId = this.config.clientId; if (!clientId) { throw new Error("Client ID not found. Please ensure it is set in config."); } const request = { client_id: clientId, ...options?.title && { title: options.title }, ...options?.expiresInHours && { expires_in_hours: options.expiresInHours } }; const response = await this.fetch(`${this.config.apiUrl}/api/chat/share/${chatId}`, { method: "POST", body: JSON.stringify(request) }); return response.json(); } async getSharedChat(shareId) { const response = await this.fetch(`${this.config.apiUrl}/api/chat/shared/${shareId}`); return response.json(); } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } getConfig() { return { ...this.config }; } // Basic fetch with API key async fetch(url, options = {}) { const headers = new Headers({ "Content-Type": "application/json", ...this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }, ...options.headers }); const response = await fetch(url, { ...options, headers }); if (!response.ok) { if (response.status === 429) { throw new Error("RATE_LIMIT_EXCEEDED"); } throw new Error(`API request failed: ${response.statusText}`); } return response; } }; var DEFAULT_CONTEXT = { isLoading: false, error: null, isLoadingChats: false, isLoadingMoreMessages: false, currentChatId: null, chats: [], messages: [], isGenerating: false, isTyping: false, hasMoreMessages: false, fastMode: false, isOpen: false, openChat: () => { }, closeChat: () => { }, createNewChat: async () => { }, selectChat: () => { }, deleteChat: async () => { }, updateChatTitle: async () => { }, sendMessage: async () => { }, stopGenerating: () => { }, retryLastMessage: async () => { }, clearHistory: () => { }, copyMessage: () => { }, provideFeedback: async () => { }, loadPreviousMessages: async () => { }, loadChats: async () => { }, config: { apiKey: "", apiUrl: "" }, updateConfig: () => { }, setFastMode: () => { }, shareChat: async () => ({ share_id: "", share_url: "" }), getSharedChat: async () => ({ share_id: "", title: "", messages: [], created_at: "" }), loadSharedChat: async () => { }, isSharedChatMode: false, sharedChatId: null }; var ChatbotContext = createContext(DEFAULT_CONTEXT); var updateChatMessages = (chats, chatId, messages) => { return chats.map((chat) => { if (chat.id === chatId) { return { ...chat, messages, lastMessage: messages[messages.length - 1]?.content }; } return chat; }); }; var ChatbotProvider = ({ config: initialConfig, children, onError }) => { const clientRef = useRef(new ChatbotClient(initialConfig)); const [isLoading, setIsLoading] = useState(false); const [isLoadingChats, setIsLoadingChats] = useState(false); const [isLoadingMoreMessages, setIsLoadingMoreMessages] = useState(false); const [error, setError] = useState(null); const [currentChatId, setCurrentChatId] = useState(null); const [chats, setChats] = useState([]); const [messages, setMessages] = useState([]); const [isGenerating, setIsGenerating] = useState(false); const [isTyping, setIsTyping] = useState(false); const [hasMoreMessages, setHasMoreMessages] = useState(false); const [fastMode, setFastMode] = useState(initialConfig.fastMode ?? false); const [isOpen, setIsOpen] = useState(false); const [config, setConfig] = useState(initialConfig); const [isSharedChatMode, setIsSharedChatMode] = useState(false); const [sharedChatId, setSharedChatId] = useState(null); const abortControllerRef = useRef(null); const handleError = useCallback( (err) => { console.error("Chatbot error:", err); if (err.message === "RATE_LIMIT_EXCEEDED") { config.onRateLimitExceeded?.(); window.dispatchEvent(new CustomEvent("rateLimitExceeded")); } setError(err); onError?.(err); }, [config, onError] ); useEffect(() => { const handleRateLimit = () => { config.onRateLimitExceeded?.(); }; window.addEventListener("rateLimitExceeded", handleRateLimit); return () => window.removeEventListener("rateLimitExceeded", handleRateLimit); }, [config]); const updateConfig = useCallback((newConfig) => { setConfig((prev) => { const updated = { ...prev, ...newConfig }; clientRef.current.updateConfig(updated); return updated; }); }, []); const loadChats = useCallback(async () => { try { setIsLoadingChats(true); const response = await clientRef.current.listChats(); setChats(response); } catch (err) { handleError(err); setChats([]); } finally { setIsLoadingChats(false); } }, [handleError]); useEffect(() => { const storedClientId = localStorage.getItem("clientId"); if (storedClientId) { updateConfig({ clientId: storedClientId }); } else { const newClientId = v4(); localStorage.setItem("clientId", newClientId); updateConfig({ clientId: newClientId }); } }, [updateConfig]); useEffect(() => { if (config.clientId) { loadChats(); } }, [config.clientId, loadChats]); const openChat = useCallback(() => setIsOpen(true), []); const closeChat = useCallback(() => setIsOpen(false), []); const sendMessage = useCallback( async (content) => { const clientId = config.clientId; if (!clientId) { console.error("No clientId available yet"); setError(new Error("Client ID not initialized. Please try again.")); onError?.(new Error("Client ID not initialized. Please try again.")); return; } try { setIsGenerating(true); setIsLoading(true); const messageId = `msg-${v4()}`; const message = { id: messageId, content, role: "user", timestamp: (/* @__PURE__ */ new Date()).toISOString() }; setMessages((prev) => [...prev, message]); if (currentChatId) { setChats((prev) => updateChatMessages(prev, currentChatId, [...messages, message])); } const abortController = new AbortController(); abortControllerRef.current = abortController; const response = await clientRef.current.sendMessage(currentChatId, content, { messageId, signal: abortController.signal, ...isSharedChatMode && sharedChatId && !currentChatId && { sharedChatId } }); const headerChatId = response.headers.get("X-Chat-ID"); if (headerChatId && !currentChatId) { setCurrentChatId(headerChatId); if (isSharedChatMode) { setIsSharedChatMode(false); setSharedChatId(null); } await loadChats(); } if (response.body) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let responseText = ""; let assistantMessageId = `msg-${v4()}`; let isFirstChunk = true; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); responseText += chunk; if (isFirstChunk) { setIsGenerating(false); setIsTyping(true); const assistantMessage = { id: assistantMessageId, content: responseText, role: "assistant", timestamp: (/* @__PURE__ */ new Date()).toISOString() }; setMessages((prev) => [...prev, assistantMessage]); if (currentChatId) { setChats( (prev) => updateChatMessages(prev, currentChatId, [...messages, message, assistantMessage]) ); } isFirstChunk = false; } else { setMessages( (prev) => prev.map( (msg) => msg.id === assistantMessageId ? { ...msg, content: responseText } : msg ) ); if (currentChatId) { setChats( (prev) => prev.map( (chat) => chat.id === currentChatId ? { ...chat, messages: chat.messages.map( (msg) => msg.id === assistantMessageId ? { ...msg, content: responseText } : msg ), lastMessage: responseText } : chat ) ); } } } } } catch (err) { if (err instanceof Error && err.name === "AbortError") { console.log("Request aborted"); } else { console.error("Error sending message:", err); const error2 = err; setError(error2); onError?.(error2); } } finally { setIsGenerating(false); setIsTyping(false); setIsLoading(false); abortControllerRef.current = null; } }, [config.clientId, currentChatId, messages, loadChats, onError] ); const stopGenerating = useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } setIsGenerating(false); setIsTyping(false); }, []); const retryLastMessage = useCallback(async () => { const lastUserMessage = [...messages].reverse().find((m) => m.role === "user"); if (lastUserMessage) { await sendMessage(lastUserMessage.content); } }, [messages, sendMessage]); const clearHistory = useCallback(() => { setMessages([]); setCurrentChatId(null); }, []); const copyMessage = useCallback( (messageId) => { const message = messages.find((m) => m.id === messageId); if (message) { navigator.clipboard.writeText(message.content); } }, [messages] ); const provideFeedback = useCallback( async (messageId, feedback) => { try { const existingMessage = messages.find((msg) => msg.id === messageId); if (existingMessage?.feedback === feedback) { await clientRef.current.deleteFeedback(messageId); setMessages( (prev) => prev.map((msg) => msg.id === messageId ? { ...msg, feedback: void 0 } : msg) ); } else { await clientRef.current.provideFeedback(messageId, feedback); setMessages((prev) => prev.map((msg) => msg.id === messageId ? { ...msg, feedback } : msg)); } } catch (err) { const error2 = err; setError(error2); onError?.(error2); } }, [messages, onError] ); const loadPreviousMessages = useCallback(async () => { if (!currentChatId || isLoadingMoreMessages) return; try { setIsLoadingMoreMessages(true); const oldestMessage = messages[0]; const response = await clientRef.current.getMessages(currentChatId, oldestMessage?.id); setMessages((prev) => [...response.messages, ...prev]); setHasMoreMessages(response.hasMore); } catch (err) { console.error("Error loading previous messages:", err); const error2 = err; setError(error2); onError?.(error2); } finally { setIsLoadingMoreMessages(false); } }, [currentChatId, isLoadingMoreMessages, messages, onError]); const createNewChat = useCallback(async () => { try { const newChat = await clientRef.current.createChat(); setChats((prev) => [newChat, ...prev]); setCurrentChatId(newChat.id); setMessages([]); } catch (err) { console.error("Error creating new chat:", err); const error2 = err; setError(error2); onError?.(error2); } }, [onError]); const selectChat = useCallback( async (chatId) => { try { setIsLoading(true); const chat = await clientRef.current.getChat(chatId); setCurrentChatId(chatId); setMessages(chat.messages || []); } catch (err) { console.error("Error selecting chat:", err); const error2 = err; setError(error2); onError?.(error2); } finally { setIsLoading(false); } }, [onError] ); const deleteChat = useCallback( async (chatId) => { try { await clientRef.current.deleteChat(chatId); setChats((prev) => prev.filter((chat) => chat.id !== chatId)); if (currentChatId === chatId) { setCurrentChatId(null); setMessages([]); } } catch (err) { console.error("Error deleting chat:", err); const error2 = err; setError(error2); onError?.(error2); } }, [currentChatId, onError] ); const updateChatTitle = useCallback( async (chatId, title) => { try { await clientRef.current.updateChatTitle(chatId, title); setChats((prev) => prev.map((chat) => chat.id === chatId ? { ...chat, title } : chat)); } catch (err) { console.error("Error updating chat title:", err); const error2 = err; setError(error2); onError?.(error2); } }, [onError] ); const shareChat = useCallback( async (chatId, options) => { try { return await clientRef.current.shareChat(chatId, options); } catch (err) { const error2 = err; setError(error2); onError?.(error2); throw error2; } }, [onError] ); const getSharedChat = useCallback( async (shareId) => { try { return await clientRef.current.getSharedChat(shareId); } catch (err) { const error2 = err; setError(error2); onError?.(error2); throw error2; } }, [onError] ); const loadSharedChat = useCallback( async (shareId) => { try { setIsLoading(true); const sharedChat = await clientRef.current.getSharedChat(shareId); setIsSharedChatMode(true); setSharedChatId(shareId); setCurrentChatId(null); setMessages(sharedChat.messages); } catch (err) { const error2 = err; setError(error2); onError?.(error2); throw error2; } finally { setIsLoading(false); } }, [onError] ); const setFastModeWithConfig = useCallback( (enabled) => { setFastMode(enabled); updateConfig({ fastMode: enabled }); }, [updateConfig] ); const contextValue = { isLoading, error, isLoadingChats, isLoadingMoreMessages, currentChatId, chats, messages, isGenerating, isTyping, hasMoreMessages, fastMode, isOpen, openChat, closeChat, createNewChat, selectChat, deleteChat, updateChatTitle, sendMessage, stopGenerating, retryLastMessage, clearHistory, copyMessage, provideFeedback, loadPreviousMessages, loadChats, config, updateConfig, setFastMode: setFastModeWithConfig, shareChat, getSharedChat, loadSharedChat, isSharedChatMode, sharedChatId }; return /* @__PURE__ */ jsx(ChatbotContext.Provider, { value: contextValue, children }); }; function useChatbot() { const context = useContext(ChatbotContext); if (!context) { throw new Error("useChatbot must be used within a ChatbotProvider"); } return context; } function useChatHistory() { const context = useContext(ChatbotContext); if (!context) { throw new Error("useChatHistory must be used within a ChatbotProvider"); } return { chats: context.chats, createNewChat: context.createNewChat, selectChat: context.selectChat, deleteChat: context.deleteChat, updateChatTitle: context.updateChatTitle }; } export { ChatbotClient, ChatbotContext, ChatbotProvider, RagProvider, useChatHistory, useChatbot };