Skip to content

@msflib/react-documents

Standalone documents module built on TanStack Query.

Install

pnpm add @msflib/react-documents

Peer deps

  • react >= 18
  • @tanstack/react-query >= 5

Usage

Wrap your app with the module Provider inside your QueryClientProvider.

In Next.js App Router, Provider must be in a client component ("use client").

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { DocumentsProvider, useDocuments } from '@msflib/react-documents';

const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  <DocumentsProvider
    options={{ requireAuth: true, isAuthenticated: true, jobsParams: { limit: 20 } }}
  >
    <DocumentsPanel />
  </DocumentsProvider>
</QueryClientProvider>;

function DocumentsPanel() {
  const { documents, ingestionStatus, ingestionJobs, uploadDocument, promoteDocument } =
    useDocuments();

  async function upload(file: File) {
    const formData = new FormData();
    formData.append('file', file, file.name);
    await uploadDocument(formData);
  }

  return null;
}

Upload → promote flow

Uploading a document queues it for ingestion. A record starts private to the uploader (or scoped to a conversation) until it is promoted into the shared workspace corpus.

import { useDocuments } from '@msflib/react-documents';

function UploadForm({ conversationId }: { conversationId: string }) {
  const { uploadDocument, promoteDocument } = useDocuments();

  async function handleUpload(file: File) {
    const formData = new FormData();
    formData.append('file', file, file.name);
    formData.append('conversation_id', conversationId);
    formData.append('is_private', 'true');

    const { record_id } = await uploadDocument(formData);
    // Later, once the document should be visible workspace-wide:
    await promoteDocument(record_id, { clear_private: true });
  }

  return null;
}

Listing, inspecting and removing documents

documents (from the Provider's default list query) reflects options.listParams (conversation_id, sub_thread_id, status, include_deleted, limit, offset). Use listDocuments to refetch with different params, getDocument/useDocument to fetch a single record, deleteDocument to soft-delete, and downloadDocument to fetch the file content as a Blob.

import { useDocuments, useDocument } from '@msflib/react-documents';

function DocumentsList() {
  const { documents, listDocuments, deleteDocument, downloadDocument } =
    useDocuments();

  async function refreshFailed() {
    await listDocuments({ status: 'failed' });
  }

  async function removeAndDownload(recordId: number) {
    await deleteDocument(recordId);
    const blob = await downloadDocument(recordId);
    // e.g. trigger a browser download via URL.createObjectURL(blob)
  }

  return null;
}

function DocumentDetail({ recordId }: { recordId: number }) {
  const { data: document } = useDocument(recordId);
  return null;
}

Reindexing

reindexDocuments re-enqueues in-scope documents for ingestion — useful after switching a workspace's vector store backend, so already-indexed documents get re-resolved and re-ingested against the new backend. Pass purge_old_profile_id to also purge the scope from the previous backend once re-enqueueing succeeds.

import { useDocuments } from '@msflib/react-documents';

function VectorStoreSwitch() {
  const { reindexDocuments } = useDocuments();

  async function onSwitch(oldProfileId: number) {
    const { scanned, enqueued, errors } = await reindexDocuments({
      purge_old_profile_id: oldProfileId,
    });
  }

  return null;
}

Ingestion monitoring

ingestionStatus summarizes queue depth and record counts by state. ingestionJobs lists individual ingestion jobs (with retry/error detail) and can be filtered by status.

import { useDocuments, useDocumentIngestionJobs } from '@msflib/react-documents';

function IngestionQueue() {
  const { ingestionStatus, runWorker } = useDocuments();
  const { data } = useDocumentIngestionJobs({ status: 'failed', limit: 50 });

  return null;
}

runWorker triggers a batch of pending ingestion jobs to be processed (useful for manually kicking the worker in admin tooling); it is not workspace-scoped.

Endpoints

Configure paths through configureApplication({ endpoints: { documents } }).

Key Default path Methods / routes
documents /documents base path used for scoping/matching; GET (list)
upload /documents/upload POST (multipart/form-data)
worker /documents/worker/run POST
reindex /documents/reindex POST
ingestionStatus /documents/ingestion/status GET
ingestionJobs /documents/ingestion/jobs GET

Additional routes built off documents: - ${documents}/{record_id} — GET (fetch one), DELETE (remove) - ${documents}/{record_id}/download — GET (raw file content) - ${documents}/{record_id}/promote — POST

Notes: - listDocuments/documents, getDocument/useDocument, deleteDocument, downloadDocument, uploadDocument, reindexDocuments, getIngestionStatus, and getIngestionJobs are workspace-scoped (they send the workspace header). - promoteDocument and runWorker are not workspace-scoped, matching the backend API contract. - downloadDocument bypasses the JSON API client and fetches the file directly (with the same auth/workspace headers) to return a Blob.