Corpus-Optional Features¶
Overview¶
OpenContracts supports viewing and interacting with documents both within and outside of corpus contexts. This document describes which features require corpus membership and how to implement corpus-optional functionality.
Feature Classification¶
Always Available (Document-Level)¶
These features work with or without corpus context:
- Document Viewing: PDF/TXT rendering
- Basic Search: Search within document
- Notes: Personal notes on documents
- Document Metadata: Title, creator, dates
- Export/Download: Save document locally
- Navigation: Page navigation, zoom
Corpus-Required Features¶
These features only work within corpus context:
- Annotations: Require corpus label sets
- Collaborative Summaries: Multi-perspective analysis
- Analyses: Corpus-scoped processing
- Extracts: Corpus-based data extraction
- Shared Comments: Team collaboration
- Label Management: Annotation types and colors
Progressive Enhancement¶
Features that enhance when corpus is added:
- Chat: Basic chat without corpus, history with corpus
- Permissions: Document permissions alone, or corpus permissions override
- Sharing: Limited without corpus, full sharing within corpus
Implementation Strategy¶
Feature Availability Hook¶
export const useFeatureAvailability = (corpusId?: string) => {
const isFeatureAvailable = (feature: string): boolean => {
const config = FEATURE_FLAGS[feature];
return !config.requiresCorpus || Boolean(corpusId);
};
return {
isFeatureAvailable,
hasCorpus: Boolean(corpusId),
getFeatureStatus: (feature: string) => ({
available: isFeatureAvailable(feature),
message: config.disabledMessage
})
};
};
Conditional GraphQL Queries¶
const DocumentKnowledgeBase = ({ documentId, corpusId }) => {
// Use different queries based on corpus availability
const { data } = useQuery(
corpusId ? GET_DOCUMENT_WITH_CORPUS : GET_DOCUMENT_ONLY,
{
variables: corpusId
? { documentId, corpusId }
: { documentId }
}
);
};
Component Adaptation¶
// Annotations only with corpus
const annotations = corpusId ? data?.annotations : [];
// Conditional handler
const handleCreateAnnotation = useCallback(
async (annotation) => {
if (!corpusId) {
toast.info('Add document to corpus to create annotations');
return;
}
await createAnnotation(annotation);
},
[corpusId, createAnnotation]
);
// Conditional rendering
return (
<>
{/* Always show document viewer */}
<DocumentViewer document={document} />
{/* Corpus-required features */}
{corpusId && (
<>
<AnnotationPanel annotations={annotations} />
<AnalysesPanel corpusId={corpusId} />
</>
)}
{/* Corpus-optional features */}
<NotesPanel documentId={documentId} corpusId={corpusId} />
{/* Add to corpus prompt when not in corpus */}
{!corpusId && (
<AddToCorpusPrompt documentId={documentId} />
)}
</>
);
Feature Flags Configuration¶
export const FEATURE_FLAGS = {
ANNOTATIONS: {
requiresCorpus: true,
displayName: 'Annotations',
disabledMessage: 'Add to corpus to annotate'
},
NOTES: {
requiresCorpus: false,
displayName: 'Notes',
disabledMessage: null
},
CHAT: {
requiresCorpus: false, // Basic chat works without corpus
enhancedWithCorpus: true, // But better with corpus
displayName: 'Document Chat'
},
ANALYSES: {
requiresCorpus: true,
displayName: 'Document Analyses',
disabledMessage: 'Add to corpus to run analyses'
}
};
UI Patterns¶
Empty State with CTA¶
const CorpusRequiredEmptyState = ({ feature, onAddToCorpus }) => (
<EmptyState>
<Icon name="folder open" size="huge" />
<Header>{feature} requires corpus membership</Header>
<p>Add this document to a corpus to enable {feature}.</p>
<Button primary onClick={onAddToCorpus}>
Add to Corpus
</Button>
</EmptyState>
);
Adaptive Controls¶
const FloatingControls = ({ corpusId, documentId }) => {
const { isFeatureAvailable } = useFeatureAvailability(corpusId);
return (
<ControlsContainer>
{/* Always available */}
<Button icon="search" title="Search" />
<Button icon="note" title="Add note" />
{/* Show add to corpus if needed */}
{!corpusId && (
<Button icon="folder plus" title="Add to corpus" primary />
)}
{/* Corpus features */}
{isFeatureAvailable('ANNOTATIONS') && (
<Button icon="highlighter" title="Annotate" />
)}
</ControlsContainer>
);
};
Progressive Disclosure¶
const DocumentPanel = ({ corpusId }) => {
const [showCorpusFeatures, setShowCorpusFeatures] = useState(false);
useEffect(() => {
// Reveal corpus features with animation
if (corpusId) {
setShowCorpusFeatures(true);
}
}, [corpusId]);
return (
<Panel>
<BasicFeatures />
{showCorpusFeatures && (
<AnimatedReveal>
<CorpusFeatures />
</AnimatedReveal>
)}
</Panel>
);
};
Add to Corpus Flow¶
Modal Component¶
const AddToCorpusModal = ({ documentId, open, onClose, onSuccess }) => {
const { data } = useQuery(GET_MY_CORPUSES);
const [addDocument] = useMutation(ADD_DOCUMENT_TO_CORPUS);
const handleAdd = async (corpusId) => {
const result = await addDocument({
variables: { documentId, corpusId }
});
if (result.data.success) {
onSuccess(corpusId);
toast.success('Document added to corpus');
}
};
return (
<Modal open={open} onClose={onClose}>
<Modal.Header>Add Document to Corpus</Modal.Header>
<Modal.Content>
<CorpusList
corpuses={data?.corpuses}
onSelect={handleAdd}
/>
</Modal.Content>
</Modal>
);
};
Success Handling¶
const handleAddToCorpusSuccess = (newCorpusId) => {
// Option 1: Reload with corpus context
window.location.href = `/corpus/${newCorpusId}/document/${documentId}`;
// Option 2: Update state and refetch
setCorpusId(newCorpusId);
refetch({ documentId, corpusId: newCorpusId });
// Option 3: Show success message and update UI
setShowSuccessMessage(true);
setCorpusFeatures(true);
};
Performance Considerations¶
Lighter Initial Load¶
- Skip corpus data when not needed
- No annotation rendering overhead
- Fewer GraphQL queries
- No WebSocket connections for collaboration
Progressive Loading¶
// Load corpus features only when needed
const { data: corpusData, loading } = useQuery(
GET_CORPUS_FEATURES,
{
skip: !corpusId,
variables: { corpusId }
}
);
Caching Strategy¶
// Cache user's corpuses for quick add-to-corpus
const { data: cachedCorpuses } = useQuery(GET_MY_CORPUSES, {
fetchPolicy: 'cache-first'
});
Testing Corpus-Optional Features¶
Test Without Corpus¶
it('should show document without corpus features', () => {
render(<DocumentKnowledgeBase documentId="123" />);
expect(screen.getByTestId('document-viewer')).toBeInTheDocument();
expect(screen.queryByTestId('annotation-panel')).not.toBeInTheDocument();
expect(screen.getByText('Add to Corpus')).toBeInTheDocument();
});
Test Corpus Addition¶
it('should enable features after adding to corpus', async () => {
const { rerender } = render(
<DocumentKnowledgeBase documentId="123" />
);
// Add to corpus
fireEvent.click(screen.getByText('Add to Corpus'));
fireEvent.click(screen.getByText('My Research Corpus'));
// Rerender with corpus
rerender(
<DocumentKnowledgeBase documentId="123" corpusId="456" />
);
// Features now available
await waitFor(() => {
expect(screen.getByTestId('annotation-panel')).toBeInTheDocument();
});
});
Migration Path¶
Phase 1: Make corpusId Optional¶
- Update DocumentKnowledgeBase props
- Add conditional GraphQL queries
- Implement feature availability checks
Phase 2: Build UI Components¶
- Create AddToCorpusModal
- Add empty states with CTAs
- Implement adaptive controls
Phase 3: Test and Polish¶
- Test all corpus-optional scenarios
- Add loading states
- Implement success animations
- Handle edge cases