npm package: @mencraft/lore-headless
Description: A headless library for chat streaming functionality
Latest version: 0.1.22

# 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