This guide explains how to integrate the Captide API into your React frontend application.
Never expose your Captide API key in frontend code! Always use a backend proxy as shown in the Backend Integration guide.

React Integration

Basic Document Viewer Setup

Here’s how to set up the Captide document viewer component in a React application:
import React, { useState } from 'react';
import { DocumentViewer, DocumentViewerProvider } from 'captide';

function App() {
  // Function to fetch document through your backend API
  const fetchDocument = async (sourceLink) => {
    const response = await fetch('/api/document', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ source_link: sourceLink })
    });
    
    return response.json();
  };

  return (
    <div className="app">
      <h1>Captide Document Explorer</h1>
      
      <DocumentViewerProvider fetchDocumentFn={fetchDocument}>
        <DocumentExplorerDemo />
      </DocumentViewerProvider>
    </div>
  );
}

function DocumentExplorerDemo() {
  const [answer, setAnswer] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [query, setQuery] = useState('');
  
  // Create a simple form for asking questions
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    
    try {
      // Call your backend API that proxies to Captide
      const response = await fetch('/api/document-snippets', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      });
      
      const data = await response.json();
      // Process and display the response
      // This is a simplified example - you'd typically render the snippets
      setAnswer(JSON.stringify(data, null, 2));
    } catch (error) {
      console.error('Error:', error);
    } finally {
      setIsLoading(false);
    }
  };
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Ask about SEC filings (e.g., What's AAPL's revenue?)"
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Ask'}
        </button>
      </form>
      
      {answer && (
        <pre>{answer}</pre>
      )}
      
      {/* The DocumentViewer needs to be sized explicitly */}
      <div style={{ height: '600px', marginTop: '20px', border: '1px solid #ccc' }}>
        <DocumentViewer />
      </div>
    </div>
  );
}

export default App;

Streaming Response Example

For handling streaming responses from the AI agent:
import React, { useState, useEffect, useRef } from 'react';
import { DocumentViewer, DocumentViewerProvider, useDocumentViewer } from 'captide';

function StreamingDemo() {
  const [query, setQuery] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [streamedText, setStreamedText] = useState('');
  const [idMapping, setIdMapping] = useState({});
  const { loadDocument } = useDocumentViewer();
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsLoading(true);
    setStreamedText('');
    
    try {
      // Call your backend API that proxies the streaming endpoint
      const response = await fetch('/api/query-stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      });
      
      // Create a reader for the stream
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        
        const text = decoder.decode(value);
        // Process the SSE data
        const lines = text.split('\n');
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.substring(6));
              
              // Handle different event types
              if (data.type === 'id_mapping') {
                setIdMapping(data.mapping);
              } else if (data.type === 'markdown_chunk') {
                setStreamedText(prev => prev + data.content);
              } else if (data.type === 'full_answer') {
                setStreamedText(data.content);
              }
            } catch (e) {
              console.error('Error parsing SSE data:', e);
            }
          }
        }
      }
    } catch (error) {
      console.error('Error with streaming:', error);
    } finally {
      setIsLoading(false);
    }
  };
  
  // Function to handle source link clicks
  const handleSourceClick = async (sourceId) => {
    // Extract source info from the mapping
    const sourceInfo = idMapping[sourceId];
    if (sourceInfo && sourceInfo.sourceLink) {
      await loadDocument(sourceInfo.sourceLink, sourceId);
    }
  };
  
  // Simple regex to detect source references like [#123abc]
  const renderWithSourceLinks = (text) => {
    if (!text) return '';
    
    // Split by references [#id] and render as clickable spans
    const parts = text.split(/(\[#[a-z0-9]+\])/g);
    
    return parts.map((part, index) => {
      // Check if this part is a reference
      const match = part.match(/\[#([a-z0-9]+)\]/);
      
      if (match) {
        const sourceId = `#${match[1]}`;
        return (
          <span 
            key={index}
            className="source-link"
            onClick={() => handleSourceClick(sourceId)}
            style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline' }}
          >
            [{index + 1}]
          </span>
        );
      }
      
      // Regular text
      return <span key={index}>{part}</span>;
    });
  };
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Ask about SEC filings (e.g., What's AAPL's revenue?)"
          style={{ width: '400px' }}
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Ask'}
        </button>
      </form>
      
      <div className="answer-container">
        {renderWithSourceLinks(streamedText)}
      </div>
      
      <div style={{ height: '600px', marginTop: '20px', border: '1px solid #ccc' }}>
        <DocumentViewer />
      </div>
    </div>
  );
}

function App() {
  const fetchDocument = async (sourceLink) => {
    const response = await fetch('/api/document', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ source_link: sourceLink })
    });
    
    return response.json();
  };

  return (
    <DocumentViewerProvider fetchDocumentFn={fetchDocument}>
      <StreamingDemo />
    </DocumentViewerProvider>
  );
}

export default App;

Next.js Integration

If you’re using Next.js, you can create API routes to proxy requests to Captide:
// pages/api/document.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  try {
    const { source_link } = req.body;
    
    const response = await fetch(source_link, {
      headers: {
        'Authorization': `Bearer ${process.env.CAPTIDE_API_KEY}`
      }
    });
    
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    
    const data = await response.json();
    res.json(data);
  } catch (error) {
    console.error('Error fetching document:', error);
    res.status(500).json({ error: 'Failed to fetch document' });
  }
}
To see a live example of a user interface built with the Captide API and document viewer, visit app.captide.co/chat.