UNPKG

@mencraft/lore-headless

Version:
662 lines (536 loc) 19.2 kB
# Lore Headless A headless library for chat streaming functionality that works with React and other frontend frameworks. [![npm version](https://badge.fury.io/js/@mencraft%2Flore-headless.svg)](https://badge.fury.io/js/@mencraft%2Flore-headless) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## Installation ```bash npm install @mencraft/lore-headless ``` ## Configuration When using this library, you'll need to provide the following authentication values: ```typescript // Authentication credentials const auth = { clientId: 'your-client-id', // Your client ID token: 'your-firebase-id-token' // Your Firebase ID token }; // Graph ID for chat operations const graphId = 'your-graph-id'; // Your graph ID ``` ### API URLs By default, the API client is configured for production URLs. You can specify different modes or use custom URLs: ```typescript // Default usage (production mode) const apiClient = new LoreClient(); // Specify a different mode const stagingClient = new LoreClient('staging'); const devClient = new LoreClient('development'); // Use custom URLs (mode will be ignored when custom URLs are provided) const customClient = new LoreClient( 'production', // This will be ignored when custom URLs are provided 'https://your-custom-chat-url.com', 'https://your-custom-validation-url.com' ); ``` **Available modes:** - `'production'` (default): Uses production API URLs - `'staging'`: Uses staging API URLs - `'development'`: Uses local development URLs (localhost) **Custom URLs:** When providing custom URLs, both `customChatUrl` and `customValidationUrl` must be provided together. If custom URLs are specified, the mode parameter will be ignored. In a real application, you would typically set the authentication values using environment variables: ```typescript // Authentication credentials const auth = { clientId: process.env.CLIENT_ID, token: process.env.FIREBASE_TOKEN }; // Graph ID for chat operations const graphId = process.env.GRAPH_ID; ``` ## Usage ### Core API (Framework Agnostic) ```typescript import { LoreClient, ChatStreamManager, ChatRequest, ChatStreamCallbacks } from 'lore-headless'; // Create an API client // Default usage (production mode) const apiClient = new LoreClient(); // Or specify a different mode // const apiClient = new LoreClient('staging'); // const apiClient = new LoreClient('development'); // Or use custom URLs // const apiClient = new LoreClient( // 'production', // mode will be ignored // 'https://your-custom-chat-url.com', // 'https://your-custom-validation-url.com' // ); // Set up authentication const auth = { clientId: 'your-client-id', token: 'your-firebase-id-token' }; // Define callbacks const callbacks: ChatStreamCallbacks = { onTextStream: (text) => { console.log('Received text:', text); }, onBatchData: (data) => { console.log('Received batch data:', data); }, onError: (error) => { console.error('Error:', error); }, onEnd: () => { console.log('Stream ended'); } }; // Create a chat stream manager const chatManager = new ChatStreamManager(apiClient, auth, callbacks); // Start a chat stream const request: ChatRequest = { user: 'user-id', session: 'session-id', query: 'Hello, how are you?' }; chatManager.startChatStream(request, 'graph-id'); // Cancel the stream if needed chatManager.cancelStream(); // Send feedback for a specific assistant message // This should be called for each assistant message that receives feedback chatManager.sendFeedback({ user: 'user-id', session: 'session-id', query: 'Hello, how are you?', // The user message that prompted the assistant's response response: 'I am doing well, thank you!', // The assistant's response being rated feedback: 'Great response!', // Optional feedback text feedback_value: 1 // Positive (1) or negative (-1) rating }, 'graph-id'); ``` ### React Hooks To use the React hooks, you need to import them from the React entry point: #### Callback-based Approach (Original) ```tsx import React, { useState } from 'react'; import { LoreClient, ChatRequest } from 'lore-headless'; import { useChatStream, useFeedback } from 'lore-headless/react'; // Message interface with unique ID interface Message { id: string; text: string; sender: 'user' | 'assistant' | 'system' | 'error'; timestamp: Date; } // Feedback modal interface interface FeedbackModalState { isOpen: boolean; messageId: string; assistantMessage: string; userMessage: string; } const ChatComponent = () => { // Messages state const [messages, setMessages] = useState<Message[]>([]); // Feedback modal state const [feedbackModal, setFeedbackModal] = useState<FeedbackModalState>({ isOpen: false, messageId: '', assistantMessage: '', userMessage: '' }); // Feedback form state const [feedbackText, setFeedbackText] = useState(''); const [feedbackRating, setFeedbackRating] = useState<number | null>(null); // Create an API client // Default usage (production mode) const apiClient = new LoreClient(); // Or specify a different mode // const apiClient = new LoreClient('staging'); // const apiClient = new LoreClient('development'); // Or use custom URLs // const apiClient = new LoreClient( // 'production', // mode will be ignored // 'https://your-custom-chat-url.com', // 'https://your-custom-validation-url.com' // ); // Set up authentication const auth = { clientId: 'your-client-id', token: 'your-firebase-id-token' }; // Use the chat stream hook const { startChatStream, cancelStream } = useChatStream(apiClient, auth, { onTextStream: (text) => { setMessages((prev) => { const lastMessage = prev[prev.length - 1]; if (lastMessage?.sender === 'assistant') { return [ ...prev.slice(0, -1), { ...lastMessage, text: lastMessage.text + text } ]; } return [ ...prev, { id: Date.now().toString(), text, sender: 'assistant', timestamp: new Date() } ]; }); }, onError: (error) => { console.error('Stream error:', error); }, onEnd: () => { console.log('Stream ended'); } }); // Use the feedback hook const { sendFeedback, loading } = useFeedback(apiClient, auth, 'graph-id'); // Handle sending a message const handleSendMessage = (message: string) => { // Add user message to the list setMessages((prev) => [ ...prev, { id: Date.now().toString(), text: message, sender: 'user', timestamp: new Date() } ]); // Start the chat stream const request: ChatRequest = { user: 'user-id', session: 'session-id', query: message }; startChatStream(request, 'graph-id'); }; // Open feedback modal for a specific message const openFeedbackModal = (messageId: string) => { const assistantMessage = messages.find(m => m.id === messageId && m.sender === 'assistant'); if (!assistantMessage) return; // Find the user message that prompted this assistant response // This is a simplified example - in a real app, you'd track which user message led to which assistant response const userMessageIndex = messages.findIndex(m => m.id === messageId) - 1; if (userMessageIndex < 0) return; const userMessage = messages[userMessageIndex]; if (userMessage.sender !== 'user') return; setFeedbackModal({ isOpen: true, messageId, assistantMessage: assistantMessage.text, userMessage: userMessage.text }); // Reset form state setFeedbackText(''); setFeedbackRating(null); }; // Close feedback modal const closeFeedbackModal = () => { setFeedbackModal(prev => ({ ...prev, isOpen: false })); }; // Submit feedback const submitFeedback = () => { if (feedbackRating === null) { alert('Please select a rating before submitting.'); return; } sendFeedback({ user: 'user-id', session: 'session-id', query: feedbackModal.userMessage, response: feedbackModal.assistantMessage, feedback: feedbackText || (feedbackRating > 0 ? 'Helpful response' : 'Not helpful response'), feedback_value: feedbackRating }); closeFeedbackModal(); }; return ( <div className="chat-container"> {/* Messages list */} <div className="messages"> {messages.map((message) => ( <div key={message.id} className={`message ${message.sender}`}> <div className="message-content">{message.text}</div> <div className="message-timestamp"> {message.timestamp.toLocaleTimeString()} </div> {/* Add feedback button to assistant messages */} {message.sender === 'assistant' && ( <button className="feedback-button" onClick={() => openFeedbackModal(message.id)} > Give Feedback </button> )} </div> ))} </div> {/* Message input form */} <form className="input-form" onSubmit={(e) => { e.preventDefault(); const input = e.currentTarget.elements.namedItem('message') as HTMLInputElement; if (input && input.value.trim()) { handleSendMessage(input.value.trim()); input.value = ''; } }} > <input type="text" name="message" placeholder="Type your message..." disabled={loading} /> <button type="submit" disabled={loading}>Send</button> </form> {/* Feedback modal */} {feedbackModal.isOpen && ( <div className="modal-overlay"> <div className="modal-content"> <div className="modal-header"> <h3>Provide Feedback</h3> <button className="close-button" onClick={closeFeedbackModal}>&times;</button> </div> <div className="modal-body"> <div className="message-context"> <div className="context-label">Assistant's response:</div> <div className="context-text">{feedbackModal.assistantMessage}</div> </div> <textarea className="feedback-textarea" placeholder="Enter your feedback here..." value={feedbackText} onChange={(e) => setFeedbackText(e.target.value)} /> <div className="rating-container"> <div className="rating-label">How would you rate this response?</div> <button className={`rating-button positive ${feedbackRating === 1 ? 'selected' : ''}`} onClick={() => setFeedbackRating(1)} > 👍 Helpful </button> <button className={`rating-button negative ${feedbackRating === -1 ? 'selected' : ''}`} onClick={() => setFeedbackRating(-1)} > 👎 Not Helpful </button> </div> <button className="submit-button" onClick={submitFeedback} disabled={loading} > Submit Feedback </button> </div> </div> </div> )} </div> ); }; ``` ## Running the Examples ### Vanilla JavaScript Example To run the vanilla JavaScript example: 1. First, build the package: ```bash npm run build ``` 2. Start the example server: ```bash node examples/vanilla/server.js ``` 3. Open your browser and navigate to the URL shown in the console. The server will automatically find an available port, starting from 3000. Note: The vanilla example uses ES modules, which require a web server to work properly due to CORS restrictions when loading modules from the file system. ### React Example The React example is provided as a reference implementation. To use it in a real React application: 1. Install the package: ```bash npm install path/to/lore-headless ``` 2. Import the components and hooks as shown in the example. ## Package Structure The package is structured to be truly headless, with React-specific functionality separated into a dedicated entry point: - **Main Entry Point**: Core functionality that works with any framework - **React Entry Point**: React-specific hooks that depend on React This structure allows you to use the package with or without React, depending on your needs. ## Development To develop this package: 1. Install dependencies: ```bash npm install ``` 2. Build the package: ```bash npm run build ``` 3. Run tests: ```bash npm test ``` ## API Reference ### Core #### `LoreClient` A client for making API requests with support for different environments. ```typescript // Default usage (production mode) const apiClient = new LoreClient(); // Specify a different mode const stagingClient = new LoreClient('staging'); const devClient = new LoreClient('development'); // Use custom URLs (mode will be ignored) const customClient = new LoreClient( 'production', // This parameter will be ignored 'https://your-custom-chat-url.com', 'https://your-custom-validation-url.com' ); ``` **Constructor Parameters:** - `mode` (string, optional): Environment mode. Options: `'production'` (default), `'staging'`, `'development'` - `customChatUrl` (string, optional): Custom chat URL. Must be provided together with `customValidationUrl` - `customValidationUrl` (string, optional): Custom validation URL. Must be provided together with `customChatUrl` **Environment Modes:** - **Production** (`'production'`): Uses production API endpoints - **Staging** (`'staging'`): Uses staging API endpoints for testing - **Development** (`'development'`): Uses local development endpoints (localhost) **Custom URLs:** When custom URLs are provided, both `customChatUrl` and `customValidationUrl` must be specified together. The `mode` parameter will be ignored in this case. #### `ChatStreamManager` A manager for handling chat streams. ```typescript const chatManager = new ChatStreamManager(apiClient, auth, callbacks); ``` ### React Hooks #### Message-based Approach (Enhanced) The library now provides an enhanced version of the `useChatStream` hook that manages messages internally and returns them as part of the hook's return value. This simplifies message management and provides a more streamlined API. ```tsx import React from 'react'; import { LoreClient, LLMRole } from 'lore-headless'; import { useChatStream, useFeedback, ChatMessage } from 'lore-headless/react'; const ChatComponent = () => { const [inputText, setInputText] = useState(''); // Use the enhanced useChatStream hook that returns messages const { messages, // Array of chat messages with different roles (user, assistant, system, error) isStreaming, // Boolean indicating if a message is currently streaming startChatStream, // Function to start a new chat stream cancelStream, // Function to cancel the current stream clearMessages // Function to clear all messages } = useChatStream( apiClient, auth, { // You can still use the callbacks if needed onSystemMessage: (text) => { console.log('System message:', text); }, } ); const handleSendMessage = async () => { if (!inputText.trim() || isStreaming) return; setInputText(''); try { await startChatStream( { user: 'user-id', session: 'session-id', query: inputText, }, 'graph-id' ); } catch (error) { console.error('Failed to start chat stream:', error); } }; return ( <div className="chat-container"> {/* Messages list */} <div className="messages"> {messages.map((message) => ( <div key={message.id} className={`message ${message.role}`} > <div className="message-header"> {message.role.charAt(0).toUpperCase() + message.role.slice(1)} {message.isStreaming && <span className="streaming-indicator" />} </div> <div className="message-content">{message.content}</div> <div className="message-timestamp"> {message.timestamp.toLocaleTimeString()} </div> </div> ))} </div> {/* Message input form */} <div className="input-container"> <input type="text" value={inputText} onChange={(e) => setInputText(e.target.value)} placeholder="Type your message..." disabled={isStreaming} /> <button onClick={handleSendMessage} disabled={isStreaming || !inputText.trim()} > Send </button> {isStreaming && ( <button onClick={cancelStream}> Cancel </button> )} <button onClick={clearMessages}> Clear Chat </button> </div> </div> ); }; ``` The `ChatMessage` interface provides a structured way to represent different types of messages: ```typescript interface ChatMessage { id: string; // Unique identifier for the message role: LLMRole | 'error'; // Role of the message sender (user, assistant, system, or error) content: string; // Content of the message timestamp: Date; // When the message was created isStreaming?: boolean; // Whether the message is currently being streamed } ``` Benefits of the message-based approach: - Automatic message management (no need to manually track messages) - Built-in support for streaming indicators - Consistent message structure with timestamps and IDs - Support for different message types (user, assistant, system, error) - Simplified API with fewer lines of code #### `useChatStream` A hook for using the chat stream in React. ```typescript import { useChatStream } from 'lore-headless/react'; // Callback-based approach (original) const { startChatStream, cancelStream } = useChatStream(apiClient, auth, callbacks); // Message-based approach (enhanced) const { messages, isStreaming, startChatStream, cancelStream, clearMessages } = useChatStream(apiClient, auth, callbacks); ``` #### `useFeedback` A hook for sending feedback in React. ```typescript import { useFeedback } from 'lore-headless/react'; const { sendFeedback, loading, error, data } = useFeedback(apiClient, auth, graphId); ``` ## License MIT