@shiftengineering/folio
v0.1.42
Published
A React component library for embedding and interacting with Folio documents, projects, and files in your application.
Keywords
Readme
@shiftengineering/folio
A React component library for embedding and interacting with Folio documents, projects, and files in your application.
Installation
# Using npm
npm install @shiftengineering/folio
# Using yarn
yarn add @shiftengineering/folio
# Using pnpm
pnpm add @shiftengineering/folioUsage
This package exports three main features:
FolioProvider- Context provider for Folio connectionsFolioEmbed- React component to embed Folio in an iframe- React hooks for interacting with Folio data:
useFolioProjects- Get projects datauseFolioFiles- Get files for a projectuseAddFolioProject- Create new projectsuseAddFolioFiles- Add files to a projectuseAddFolioDirectoriesWithFiles- Add directories with files to a projectuseFolioUserMetadata- Get and update user metadata
Secure Token Handling
By default, the component uses a secure token passing mechanism via postMessage instead of passing the JWT token as a URL query parameter. This ensures your token is not visible in network logs or browser history.
The token is passed securely as follows:
- The iframe loads without the token in the URL
- When the iframe is ready, it requests the token from the parent via postMessage
- The parent application responds with the token, which is then used for API requests
If you need backward compatibility with older versions, you can set the passTokenInQueryParam property to true on the FolioProvider:
<FolioProvider
host="http://your-folio-server.com"
port={5174}
token={token}
passTokenInQueryParam={true} // Legacy mode: passes token in URL (less secure)
>
<App />
</FolioProvider>Basic Setup
First, wrap your application with the FolioProvider:
import { FolioProvider } from "@shiftengineering/folio";
import App from "./App";
// The token must be a valid JWT that the Folio backend is configured to accept
const token = "your-jwt-auth-token";
// Optional user metadata to personalize AI responses
const userMetadata = {
role: "Sales Representative",
industry: "Healthcare",
};
ReactDOM.render(
<FolioProvider
host="http://your-folio-server.com"
port={5174}
token={token}
userMetadata={userMetadata}
>
<App />
</FolioProvider>,
document.getElementById("root"),
);Embedding Folio
Use the FolioEmbed component to embed Folio in your application:
import { FolioEmbed } from "@shiftengineering/folio";
function MyFolioPage() {
return (
<div className="folio-page">
<h1>My Folio Documents</h1>
<FolioEmbed
width="100%"
height="800px"
className="my-custom-class"
style={{ border: "1px solid #ccc" }}
/>
</div>
);
}Working with Folio Data
Use the Folio hooks to get and manipulate Folio data:
import {
useFolioProjects,
useFolioFiles,
useAddFolioProject,
useAddFolioFiles,
useAddFolioDirectoriesWithFiles,
useFolioUserMetadata,
type DirectoryEntry,
type MetadataValue,
} from "@shiftengineering/folio";
import { useState } from "react";
function FolioProjectManager() {
const [selectedProjectId, setSelectedProjectId] = useState(null);
// Get projects with loading and error states
const {
projects,
isLoading: isProjectsLoading,
error: projectsError,
} = useFolioProjects();
// Get files for the selected project
const { files, isLoading: isFilesLoading } = useFolioFiles(selectedProjectId);
// Get and update user metadata
const {
metadata: userMetadata,
updateMetadata,
isLoading: isMetadataLoading
} = useFolioUserMetadata();
// Add a new project
const { addProject, isAdding: isCreatingProject } = useAddFolioProject();
// Add files to a project
const { addFiles, isAdding: isAddingFiles } =
useAddFolioFiles(selectedProjectId);
// Add directories with files to a project
const { addDirectoriesWithFiles, isAdding: isAddingDirectory } =
useAddFolioDirectoriesWithFiles(selectedProjectId);
const handleCreateProject = () => {
addProject("My New Project");
};
const handleUpdateUserMetadata = () => {
updateMetadata({
role: "Project Manager",
industry: "Finance",
interestedIn: "State contracts"
});
};
const handleAddFile = () => {
addFiles([{
blobUrl: "/path/to/file.pdf",
name: "My Document.pdf",
userProvidedId: "doc-123" // Required unique identifier for this file
}]);
};
const handleAddSingleDirectory = () => {
// Create metadata with nested structure
const metadata = {
category: "reports",
details: {
owner: "John Doe",
department: "Finance",
tags: ["important", "quarterly"],
},
status: {
reviewed: true,
approvalDate: "2023-10-15"
}
};
const directoryEntry: DirectoryEntry = {
directoryName: "My Documents",
directoryMetadata: metadata,
files: [
{
blobUrl: "/path/to/file1.pdf",
name: "Document 1.pdf",
userProvidedId: "doc-456" // Required unique identifier for this file
},
{
blobUrl: "/path/to/file2.pdf",
name: "Document 2.pdf",
userProvidedId: "doc-789" // Required unique identifier for this file
},
],
};
addDirectoriesWithFiles(directoryEntry);
};
const handleAddMultipleDirectories = () => {
// Create metadata for each directory with nested structures
const metadata1 = {
category: "reports",
details: {
owner: "Jane Smith",
department: "Accounting",
tags: ["quarterly", "financial"]
}
};
const metadata2 = {
category: "contracts",
details: {
owner: "Legal Team",
priority: "high",
clients: ["Acme Inc", "Globex Corp"]
},
approvalChain: {
legalApproved: true,
executiveApproved: false
}
};
const directories: DirectoryEntry[] = [
{
directoryName: "Reports",
directoryMetadata: metadata1,
files: [
{
blobUrl: "/path/to/report1.pdf",
name: "Report 1.pdf",
userProvidedId: "report-1" // Required unique identifier for this file
},
{
blobUrl: "/path/to/report2.pdf",
name: "Report 2.pdf",
userProvidedId: "report-2" // Required unique identifier for this file
},
],
},
{
directoryName: "Contracts",
directoryMetadata: metadata2,
files: [
{
blobUrl: "/path/to/contract1.pdf",
name: "Contract 1.pdf",
userProvidedId: "contract-1" // Required unique identifier for this file
},
{
blobUrl: "/path/to/contract2.pdf",
name: "Contract 2.pdf",
userProvidedId: "contract-2" // Required unique identifier for this file
},
],
},
];
addDirectoriesWithFiles(directories);
};
if (isProjectsLoading) return <div>Loading projects...</div>;
if (projectsError) return <div>Error: {projectsError.message}</div>;
return (
<div>
<button onClick={handleCreateProject} disabled={isCreatingProject}>
{isCreatingProject ? "Creating..." : "Create New Project"}
</button>
<h2>Your Projects</h2>
<ul>
{projects.map((project) => (
<li
key={project.id}
onClick={() => setSelectedProjectId(project.id)}
style={{
fontWeight: project.id === selectedProjectId ? "bold" : "normal",
}}
>
{project.name}
</li>
))}
</ul>
{selectedProjectId && (
<>
<h2>Project Files</h2>
<button onClick={handleAddFile} disabled={isAddingFiles}>
{isAddingFiles ? "Adding..." : "Add File"}
</button>
<button onClick={handleAddSingleDirectory} disabled={isAddingDirectory}>
{isAddingDirectory ? "Adding..." : "Add Directory with Files"}
</button>
<button onClick={handleAddMultipleDirectories} disabled={isAddingDirectory}>
{isAddingDirectory ? "Adding..." : "Add Multiple Directories"}
</button>
{isFilesLoading ? (
<div>Loading files...</div>
) : (
<ul>
{files.map((file) => (
<li key={file.id}>
{file.name}
{file.userProvidedId && <small> (ID: {file.userProvidedId})</small>}
</li>
))}
</ul>
)}
</>
)}
</div>
);
}Google Analytics 4 Integration
Folio supports analytics event tracking that can be used with Google Analytics 4 or any other analytics provider. This feature enables tracking key user interactions and provides visibility into how users engage with the application.
Configuration
For Google Analytics 4
To enable built-in Google Analytics 4 tracking, add the measurement ID to your environment variables provided to the docker container:
VITE_GA4_MEASUREMENT_ID=G-XXXXXXXXXXWhen this environment variable is present, Folio will automatically initialize GA4 and send events to Google Analytics.
Server Environment: Configuration
If you're hosting the Folio backend yourself, you can configure various aspects of the server via environment variables.
Langfuse Observability
Folio supports Langfuse for tracing and observability of OpenAI API calls. To enable Langfuse tracing, add the following environment variables:
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_BASE_URL=https://cloud.langfuse.com # or https://us.cloud.langfuse.com for US regionWhen these variables are set, all OpenAI chat and embedding requests will be traced in Langfuse, giving you visibility into:
- Token usage and costs
- Request latency
- User sessions (when using Clerk authentication)
- Error tracking
If these variables are not set, the application will run normally without tracing.
Embedding Configuration
You can control the embeddings API behavior on a per-container basis via environment variables:
Embedding Request Rate Limit
EMBEDDING_RATE_LIMIT_PER_MINUTE=5000- What it does: Caps embedding requests at the specified number per minute for that container.
- Default:
5000if not set. - Scaling: In multi-container setups, set this per container to your desired per-container limit.
Embedding Token Rate Limit
EMBEDDING_TOKENS_PER_MINUTE=5000000- What it does: Caps the total number of tokens processed for embeddings per minute for that container.
- Default:
5000000(5 million) if not set. - Note: This helps manage costs and avoid hitting provider token-based rate limits.
Embedding Batch Size
EMBEDDING_MAX_BATCH_SIZE=100- What it does: Sets the maximum number of items to include in a single embedding batch request.
- Default:
100if not set. - Note: Larger batches are more efficient but may hit size limits with some providers.
Example with Docker Compose (build args):
services:
folio:
build:
context: .
dockerfile: Dockerfile
args:
EMBEDDING_RATE_LIMIT_PER_MINUTE: ${EMBEDDING_RATE_LIMIT_PER_MINUTE:-5000}
EMBEDDING_TOKENS_PER_MINUTE: ${EMBEDDING_TOKENS_PER_MINUTE:-5000000}
EMBEDDING_MAX_BATCH_SIZE: ${EMBEDDING_MAX_BATCH_SIZE:-100}For Any Analytics Provider
Important: Even if you don't configure GA4, you can still capture all analytics events by providing the onAnalyticsEvent callback to the FolioProvider. This gives you complete flexibility to use any analytics provider of your choice.
Events Tracked
The following events are tracked automatically:
page_view- When a user navigates to a new pagechat_sent- When a user sends a chat message (includes the query)highlight- When a user creates a highlight (includes file path and selection length)add_to_chat- When a user adds content to chat (includes file path and snippet size)extract- When content is extracted (includes file path and extractor type)switch_project- When a user switches between projectsfile_view- When a user views a file
Analytics Event Structure
All analytics events follow this structure:
export type AnalyticsEvent =
| { name: "page_view"; data: { pathname: string; projectId?: string } }
| { name: "chat_sent"; data: { query: string } }
| { name: "highlight"; data: { filePath: string; selectionLength: number } }
| { name: "add_to_chat"; data: { filePath: string; snippetSize: number } }
| { name: "extract"; data: { filePath: string; extractor: string } }
| {
name: "switch_project";
data: { fromProjectId: string; toProjectId: string };
}
| { name: "file_view"; data: { filePath: string } };Each event has:
- A
nameproperty identifying the event type - A
dataobject with event-specific parameters
Host Integration
If you're embedding Folio in your application, you can access the analytics event stream by providing the onAnalyticsEvent callback to the FolioProvider:
import { FolioProvider, AnalyticsEvent } from "@shiftengineering/folio";
function YourApp() {
const handleAnalyticsEvent = (event: AnalyticsEvent) => {
// Forward to your own analytics system or process the data
console.log(`Folio event: ${event.name}`, event.data);
// Example: Send to Google Analytics
if (window.gtag) {
window.gtag('event', event.name, event.data);
}
// Example: Send to Mixpanel
if (window.mixpanel) {
window.mixpanel.track(event.name, event.data);
}
// Example: Send to custom analytics endpoint
fetch('https://your-analytics-api.com/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
};
return (
<FolioProvider
host="http://your-folio-server.com"
port={5174}
token="your-auth-token"
onAnalyticsEvent={handleAnalyticsEvent}
>
<YourAppContent />
</FolioProvider>
);
}Alternatively, you can listen to the raw custom event directly:
window.addEventListener("folio-analytics", (event) => {
// Access the event data from event.detail
const { name, data } = event.detail;
// Forward to your own analytics system or process the data
console.log(`Folio event: ${name}`, data);
});Both approaches allow host applications to consume the same events regardless of whether GA4 is configured, enabling integration with any analytics service or custom tracking solution.
Deep Linking
Folio supports deep linking for directories, allowing users to navigate back to the original platform when Folio is embedded in an iframe. This feature is useful when you want to provide a seamless experience between your platform and Folio.
Note: Currently, only directories support deep linking. Individual files do not have deep link functionality.
Configuration
Deep linking is controlled by the VITE_ENABLE_DIRECT_DEEPLINKS environment variable, which must be provided to the container at runtime. Set this to true to enable the feature.
How It Works
When a directory is created with a userProvidedId (using the useAddFolioDirectoriesWithFiles hook), a deep link button (external link icon) will appear next to the directory in:
- The file list sidebar
- The directory viewer page
When clicked, this button emits a deep link event containing:
userProvidedId- The unique identifier that was provided when the directory was createdmetadata- The complete metadata object that was originally provided when creating the directory
Deep Link Event Structure
export type DeepLinkEvent = {
userProvidedId: string;
metadata: Record<string, unknown> | null;
};Host Integration
To handle deep link events in your host application, provide the onDeepLinkEvent callback to the FolioProvider:
import { FolioProvider, DeepLinkEvent } from "@shiftengineering/folio";
function YourApp() {
const handleDeepLinkEvent = (event: DeepLinkEvent) => {
// Navigate back to the original platform
console.log(`Deep link clicked:`, event);
console.log(`User Provided ID: ${event.userProvidedId}`);
console.log(`Metadata:`, event.metadata);
// Example: Navigate to a specific page in your platform
if (event.userProvidedId) {
window.location.href = `/records/${event.userProvidedId}`;
}
// Example: Use metadata to construct a more complex URL
if (event.metadata?.recordType === "contract") {
window.location.href = `/contracts/${event.userProvidedId}`;
}
};
return (
<FolioProvider
host="http://your-folio-server.com"
port={5174}
token="your-auth-token"
onDeepLinkEvent={handleDeepLinkEvent}
>
<YourAppContent />
</FolioProvider>
);
}API Reference
FolioProvider
Context provider that manages Folio application connection settings.
| Prop | Type | Default | Description |
| ----------------------- | ------------------------------- | -------------------- | ------------------------------------------------------------------------------------ |
| host | string | 'http://localhost' | Host for the Folio API and iframe |
| port | number | 5174 | Port for the Folio API and iframe |
| token | string | - | JWT authentication token that the Folio backend is configured to accept |
| userMetadata | Record<string, MetadataValue> | - | Optional metadata for the current user that will be used to personalize AI responses |
| onAnalyticsEvent | (event: AnalyticsEvent) => void | - | Optional callback for handling analytics events from Folio |
| onDeepLinkEvent | (event: DeepLinkEvent) => void | - | Optional callback for handling deep link events when users click directory links |
| passTokenInQueryParam | boolean | false | Whether to pass the token in URL (legacy, less secure) instead of using postMessage |
FolioEmbed
React component to embed Folio in an iframe.
| Prop | Type | Default | Description |
| ------------- | ---------------- | ------------------------------------------------------------------- | ----------------------------------------------- |
| width | string | number | '100%' | Width of the iframe |
| height | string | number | '100vh' | Height of the iframe |
| allow | string | 'camera; microphone; clipboard-read; clipboard-write; fullscreen' | Allow attributes for the iframe |
| style | object | {} | Additional styles for the iframe container |
| className | string | '' | Additional class names for the iframe container |
| iframeProps | object | {} | Additional props to pass to the iframe |
Hooks
useFolioProjects()
Hook for getting all projects for the current user.
| Return Property | Type | Description |
| --------------- | -------------------- | ------------------------------------- |
| projects | FolioProject[] | Array of projects |
| isLoading | boolean | Whether projects are being loaded |
| isError | boolean | Whether an error occurred |
| error | Error \| null | Error object if an error occurred |
| refetch | () => Promise<...> | Function to manually refetch projects |
useFolioFiles(projectId?: number)
Hook for getting all files for a specific project.
| Return Property | Type | Description |
| --------------- | -------------------- | ---------------------------------- |
| files | FolioFile[] | Array of files |
| isLoading | boolean | Whether files are being loaded |
| isError | boolean | Whether an error occurred |
| error | Error \| null | Error object if an error occurred |
| refetch | () => Promise<...> | Function to manually refetch files |
useAddFolioProject()
Hook for adding a new project.
| Return Property | Type | Description |
| ----------------- | ----------------------------------------- | -------------------------------------- |
| addProject | (name: string) => void | Function to add a project |
| addProjectAsync | (name: string) => Promise<FolioProject> | Async version returning a promise |
| isAdding | boolean | Whether a project is being added |
| isError | boolean | Whether an error occurred |
| error | Error \| null | Error object if an error occurred |
| newProject | FolioProject \| undefined | The newly created project if available |
useAddFolioFiles(projectId?: number)
Hook for adding files to a project. Files are always created at the root level (parentId = null) and are not directories.
| Return Property | Type | Description |
| --------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------- |
| addFiles | (files: { blobUrl: string; name: string; userProvidedId: string }[]) => void | Function to add files |
| addFilesAsync | (files: { blobUrl: string; name: string; userProvidedId: string }[]) => Promise<FolioFile[]> | Async version returning a promise |
| isAdding | boolean | Whether files are being added |
| isError | boolean | Whether an error occurred |
| error | Error \| null | Error object if an error occurred |
| newFiles | FolioFile[] \| undefined | The newly added files if available |
useAddFolioDirectoriesWithFiles(projectId?: number)
Hook for adding one or more directories with files to a project. Directory names must be unique at the root level (duplicates will be silently skipped with a console warning).
| Return Property | Type | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| addDirectoriesWithFiles | (params: DirectoryEntry \| DirectoryEntry[]) => void | Function to add one or more directories with files |
| addDirectoriesWithFilesAsync | (params: DirectoryEntry \| DirectoryEntry[]) => Promise<{ directory: FolioFile \| null; files: FolioFile[] } \| Array<{ directory: FolioFile \| null; files: FolioFile[] }>> | Async version returning a promise. Returns a single result when given a single directory, or an array of results when given multiple directories |
| isAdding | boolean | Whether the directories and files are being added |
| isError | boolean | Whether an error occurred |
| error | Error \| null | Error object if an error occurred |
| result | { directory: FolioFile \| null; files: FolioFile[] } \| Array<{ directory: FolioFile \| null; files: FolioFile[] }> \| undefined | The newly added directories and files. If a directory is null in a result, it means a directory with that name already existed |
useFolioUserMetadata()
Hook for retrieving and updating the current user's metadata.
| Return Property | Type | Description |
| --------------------- | ------------------------------------------------------------ | ---------------------------------------- |
| metadata | string \| null | The user's metadata as a string, or null |
| isLoading | boolean | Whether metadata is being loaded |
| isError | boolean | Whether an error occurred |
| error | Error \| null | Error object if an error occurred |
| updateMetadata | (metadata: Record<string, MetadataValue>) => void | Function to update user metadata |
| updateMetadataAsync | (metadata: Record<string, MetadataValue>) => Promise<void> | Async version returning a promise |
| isUpdating | boolean | Whether metadata is being updated |
| refetch | () => Promise<...> | Function to manually refetch metadata |
Types
The library exports these TypeScript types:
| Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FolioFile | Represents a file in Folio. Contains properties: id, name, blobUrl, parentId (null for root items), isDirectory (boolean), userProvidedId (string), createdAt (Date), and updatedAt (Date) |
| FolioProject | Represents a project in Folio. Contains properties: id, name, createdAt (Date), and updatedAt (Date) |
| MetadataValue | Represents metadata values that can be nested. Can be a string, number, boolean, null, object, or array of these types. Used for both directory metadata and user metadata. |
| DirectoryEntry | Represents a directory with metadata and files to be added to Folio. Contains properties: directoryName, directoryMetadata (now supports nested objects), and files |
| FolioFileInput | Input type for adding files to Folio. Contains properties: blobUrl, name, and userProvidedId (required for deduplication) |
| AnalyticsEvent | A union type for analytics events sent by Folio. Each event has a name property (like "page_view" or "file_view") and a data object with event-specific parameters. See the Analytics Event Structure section for details on all event types. |
| DeepLinkEvent | Event emitted when a user clicks a deep link button in Folio. Contains properties: userProvidedId (string) and metadata (Record<string, unknown> | null). See the Deep Linking section for details. |
| FolioEmbedProps | Props for the FolioEmbed component |
| FolioProviderProps | Props for the FolioProvider component |
| FolioClient | Interface for the client that interacts with the Folio API |
