A React component library for embedding and interacting with Folio documents, projects, and files in your application.
# Using npm
npm install @shiftengineering/folio
# Using yarn
yarn add @shiftengineering/folio
# Using pnpm
pnpm add @shiftengineering/folio
This package exports three main features:
-
FolioProvider
- Context provider for Folio connections -
FolioEmbed
- React component to embed Folio in an iframe - React hooks for interacting with Folio data:
-
useFolioProjects
- Get projects data -
useFolioFiles
- Get files for a project -
useAddFolioProject
- Create new projects -
useAddFolioFiles
- Add files to a project -
useAddFolioDirectoriesWithFiles
- Add directories with files to a project -
useFolioUserMetadata
- Get and update user metadata
-
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>
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"),
);
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>
);
}
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>
);
}
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.
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-XXXXXXXXXX
When this environment variable is present, Folio will automatically initialize GA4 and send events to Google Analytics.
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.
The following events are tracked automatically:
-
page_view
- When a user navigates to a new page -
chat_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 projects -
file_view
- When a user views a file
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
name
property identifying the event type - A
data
object with event-specific parameters
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.
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 |
passTokenInQueryParam |
boolean | false |
Whether to pass the token in URL (legacy, less secure) instead of using postMessage |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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. |
FolioEmbedProps |
Props for the FolioEmbed component |
FolioProviderProps |
Props for the FolioProvider component |
FolioClient |
Interface for the client that interacts with the Folio API |