OpenContracts is built in part on top of the PAWLs project frontend. We have made extensive changes, however, and plan to remove even more of the original PAWLs codebase, particularly their state management, as it's currently duplucitive of the Apollo state store we use throughout the application. That said, PAWLs was the inspiration for how we handle text extraction, and we're planning to continue using their PDF rendering code. We are also using PAWLs' pre-processing script, which is based on Grobid.
We should also thank the Grobid project, which was clearly a source of inspiration for PAWLs and an extremely impressive tool. Grobid is designed more for medical and scientific papers, but, nevertheless, offers a tremendous amount of inspiration and examples for the legal world to borrow. Perhaps there is an opportunity to have a unified tool in that respect.
Finally, let's not forget Tesseract, the OCR engine that started its life as an HP research project in the 1980s before being taken over by Google in the early aughts and finally becoming an independent project in 2018. Were it not for the excellent, free OCR provided by Tesseract, we'd have to rely on commercial OCR tech, which would make this kind of opensource, free project prohibitively expensive. Thanks to the many, many people who've made free OCR possible over the nearly 40 years Tesseract has been under development.
OpenContracts is built in part on top of the PAWLs project frontend. We have made extensive changes, however, and plan to remove even more of the original PAWLs codebase, particularly their state management, as it's currently duplucitive of the Apollo state store we use throughout the application. That said, PAWLs was the inspiration for how we handle text extraction, and we're planning to continue using their PDF rendering code. We are also using PAWLs' pre-processing script, which is based on Grobid.
We should also thank the Grobid project, which was clearly a source of inspiration for PAWLs and an extremely impressive tool. Grobid is designed more for medical and scientific papers, but, nevertheless, offers a tremendous amount of inspiration and examples for the legal world to borrow. Perhaps there is an opportunity to have a unified tool in that respect.
Finally, let's not forget Tesseract, the OCR engine that started its life as an HP research project in the 1980s before being taken over by Google in the early aughts and finally becoming an independent project in 2018. Were it not for the excellent, free OCR provided by Tesseract, we'd have to rely on commercial OCR tech, which would make this kind of opensource, free project prohibitively expensive. Thanks to the many, many people who've made free OCR possible over the nearly 40 years Tesseract has been under development.
\ No newline at end of file
diff --git a/architecture/asynchronous-processing/index.html b/architecture/asynchronous-processing/index.html
index d14f2e66..e0e28e5c 100755
--- a/architecture/asynchronous-processing/index.html
+++ b/architecture/asynchronous-processing/index.html
@@ -1,4 +1,4 @@
- Asynchronous Processing - OpenContracts
OpenContracts makes extensive use of celery, a powerful, mature python framework for distributed and asynchronous processing. Out-of-the-box, dedicated celeryworkers are configured in the docker compose stack to handle computationally-intensive and long-running tasks like parsing documents, applying annotations to pdfs, creating exports, importing exports, and more.
We are always working to make OpenContracts more fault-tolerant and stable. That said, due to the nature of the types of documents we're working with - pdfs - there is tremendous variation in what the parsers have to parse. Some documents are extremely long - thousands of pages or more - whereas other documents may have poor formatting, no text layers, etc.. In most cases, OpenContracts should be able to process the pdfs and make them compatible with our annotation tools. Sometimes, however, either due to unexpected issues or unexpected volume of documents, you may want to purge the queue of tasks to be processed by your celery workers. To do this, type:
sudo docker-compose -f local.yml run django celery -A config.celery_app purge
+ body[data-md-color-scheme="slate"] .gslide-desc { color: var(--md-default-fg-color);}
OpenContracts makes extensive use of celery, a powerful, mature python framework for distributed and asynchronous processing. Out-of-the-box, dedicated celeryworkers are configured in the docker compose stack to handle computationally-intensive and long-running tasks like parsing documents, applying annotations to pdfs, creating exports, importing exports, and more.
We are always working to make OpenContracts more fault-tolerant and stable. That said, due to the nature of the types of documents we're working with - pdfs - there is tremendous variation in what the parsers have to parse. Some documents are extremely long - thousands of pages or more - whereas other documents may have poor formatting, no text layers, etc.. In most cases, OpenContracts should be able to process the pdfs and make them compatible with our annotation tools. Sometimes, however, either due to unexpected issues or unexpected volume of documents, you may want to purge the queue of tasks to be processed by your celery workers. To do this, type:
sudo docker-compose -f local.yml run django celery -A config.celery_app purge
Be aware that this can cause some undesired effects for your users. For example, everytime a new document is uploaded, a Django signal kicks off the pdf preprocessor to produce the PAWLs token layer that is later annotated. If these tasks are in-queue and the queue is purged, you'll have documents that are not annotatable as they'll lack the PAWLS token layers. In such cases, we recommend you delete and re-upload the documents. There are ways to manually reprocess the pdfs, but we don't have a user-friendly way to do this yet.
\ No newline at end of file
diff --git a/architecture/components/annotator/how-annotations-are-created/index.html b/architecture/components/annotator/how-annotations-are-created/index.html
index 05aa3165..363d1633 100755
--- a/architecture/components/annotator/how-annotations-are-created/index.html
+++ b/architecture/components/annotator/how-annotations-are-created/index.html
@@ -1,4 +1,4 @@
- How Annotations are Handled - OpenContracts
The user selects text on the PDF by clicking and dragging the mouse. This triggers a mouse event in the Page component.
The Page component checks if the Shift key is pressed.
If the Shift key is not pressed, it creates a new selection and sets the selection state in the AnnotationStore.
If the Shift key is pressed, it adds the selection to the selection queue in the AnnotationStore.
The AnnotationStore updates its internal state with the new selection or the updated selection queue.
If the Shift key is released, the Page component triggers the creation of a multi-page annotation. If the Shift key is still pressed, it waits for the next user action.
To create a multi-page annotation, the Page component combines the selections from the queue.
The Page component retrieves the annotation data from the PDFPageInfo object for each selected page.
The Page component creates a ServerAnnotation object with the combined annotation data.
The Page component calls the createAnnotation function in the AnnotationStore, passing the ServerAnnotation object.
The AnnotationStore invokes the requestCreateAnnotation function in the Annotator component.
The Annotator component sends a mutation to the server to create the annotation.
If the server responds with success, the Annotator component updates the local state with the new annotation. If there's an error, it displays an error message.
The updated annotations trigger a re-render of the relevant components, reflecting the newly created annotation on the PDF.
The user selects text on the PDF by clicking and dragging the mouse. This triggers a mouse event in the Page component.
The Page component checks if the Shift key is pressed.
If the Shift key is not pressed, it creates a new selection and sets the selection state in the AnnotationStore.
If the Shift key is pressed, it adds the selection to the selection queue in the AnnotationStore.
The AnnotationStore updates its internal state with the new selection or the updated selection queue.
If the Shift key is released, the Page component triggers the creation of a multi-page annotation. If the Shift key is still pressed, it waits for the next user action.
To create a multi-page annotation, the Page component combines the selections from the queue.
The Page component retrieves the annotation data from the PDFPageInfo object for each selected page.
The Page component creates a ServerAnnotation object with the combined annotation data.
The Page component calls the createAnnotation function in the AnnotationStore, passing the ServerAnnotation object.
The AnnotationStore invokes the requestCreateAnnotation function in the Annotator component.
The Annotator component sends a mutation to the server to create the annotation.
If the server responds with success, the Annotator component updates the local state with the new annotation. If there's an error, it displays an error message.
The updated annotations trigger a re-render of the relevant components, reflecting the newly created annotation on the PDF.
Inside the useEffect hook that runs when the openedDocument prop changes, the PDF loading process is initiated.
The pdfjsLib.getDocument function from the pdfjs-dist library is used to load the PDF file specified by openedDocument.pdfFile.
The loading progress is tracked using the loadingTask.onProgress callback, which updates the progress state.
Once the PDF is loaded, the loadingTask.promise is resolved, and the PDFDocumentProxy object is obtained.
The PDFPageInfo objects are created for each page of the PDF using doc.getPage(i) and stored in the pages state.
Where and how are annotations loaded?
Annotations are loaded using the REQUEST_ANNOTATOR_DATA_FOR_DOCUMENT GraphQL query in the Annotator.tsx component.
The useQuery hook from Apollo Client is used to fetch the annotator data based on the provided initial_query_vars.
The annotator_data received from the query contains information about existing text annotations, document label annotations, and relationships.
The annotations are transformed into ServerAnnotation, DocTypeAnnotation, and RelationGroup objects and stored in the pdfAnnotations state using setPdfAnnotations.
Where is the PAWLs layer loaded?
The PAWLs layer is loaded in the Annotator.tsx component.
Inside the useEffect hook that runs when the openedDocument prop changes, the PAWLs layer is loaded using the getPawlsLayer function from api/rest.ts.
The getPawlsLayer function makes an HTTP GET request to fetch the PAWLs data file specified by openedDocument.pawlsParseFile.
The PAWLs data is expected to be an array of PageTokens objects, which contain token information for each page of the PDF.
The loaded PAWLs data is then used to create PDFPageInfo objects for each page, which include the page tokens.
The PDFView component is a top-level component that renders the PDF document with annotations, relations, and text search capabilities. It manages the state and functionality related to annotations, relations, and user interactions. Here's a detailed explanation of how the component works:
The PDFView component receives several props, including permissions, callbacks for CRUD operations on annotations and relations, refs for container and selection elements, and various configuration options.
It initializes several state variables using the useState hook, including:
selectionElementRefs and searchResultElementRefs: Refs for annotation selections and search results.
pageElementRefs: Refs for individual PDF pages.
scrollContainerRef: Ref for the scroll container.
textSearchMatches and searchText: State for text search matches and search text.
selectedAnnotations and selectedRelations: State for currently selected annotations and relations.
pageSelection and pageSelectionQueue: State for current page selection and queued selections.
pdfPageInfoObjs: State for PDF page information objects.
Various other state variables for active labels, relation modal visibility, and annotation options.
The component defines several functions for updating state and handling user interactions, such as:
insertSelectionElementRef, insertSearchResultElementRefs, and insertPageRef: Functions to add refs for selections, search results, and pages.
onError: Error handling callback.
advanceTextSearchMatch and reverseTextSearchMatch: Functions to navigate through text search matches.
onRelationModalOk and onRelationModalCancel: Callbacks for relation modal actions.
createMultiPageAnnotation: Function to create a multi-page annotation from queued selections.
The component uses the useEffect hook to handle side effects, such as:
Setting the scroll container ref on load.
Listening for changes in the shift key and triggering annotation creation.
Updating text search matches when the search text changes.
The component renders the PDF document and its related components using the PDFStore and AnnotationStore contexts:
The PDFStore context provides the PDF document, pages, and error handling.
The AnnotationStore context provides annotation-related state and functions.
The component renders the following main sections:
LabelSelector: Allows the user to select the active label for annotations.
DocTypeLabelDisplay: Displays the document type labels.
AnnotatorSidebar: Sidebar component for managing annotations and relations.
AnnotatorTopbar: Top bar component for additional controls and options.
PDF: The actual PDF component that renders the PDF pages and annotations.
The PDF component, defined in PDF.tsx, is responsible for rendering the PDF pages and annotations. It receives props from the PDFView component, such as permissions, configuration options, and callbacks.
The PDF component maps over each page of the PDF document and renders a Page component for each page, passing the necessary props.
The Page component, also defined in PDF.tsx, is responsible for rendering a single page of the PDF document along with its annotations and search results. It handles mouse events for creating and modifying annotations.
The PDFView component also renders the RelationModal component when the active relation label is set and the user has the necessary permissions. The modal allows the user to create or modify relations between annotations.
PDF renders the actual PDF document with annotations and text search capabilities. PDFView (see above) is what actually interacts with the backend / API.
The PDF component receives several props:
shiftDown: Indicates whether the Shift key is pressed (optional).
doc_permissions and corpus_permissions: Specify the permissions for the document and corpus, respectively.
read_only: Determines if the component is in read-only mode.
show_selected_annotation_only: Specifies whether to show only the selected annotation.
show_annotation_bounding_boxes: Specifies whether to show annotation bounding boxes.
show_annotation_labels: Specifies the behavior for displaying annotation labels.
setJumpedToAnnotationOnLoad: A callback function to set the jumped-to annotation on load.
The PDF component retrieves the PDF document and pages from the PDFStore context.
It maps over each page of the PDF document and renders a Page component for each page, passing the necessary props.
The Page component is responsible for rendering a single page of the PDF document along with its annotations and search results.
Inside the Page component:
It creates a canvas element using the useRef hook to render the PDF page.
It retrieves the annotations for the current page from the AnnotationStore context.
It defines a ConvertBoundsToSelections function that converts the selected bounds to annotations and tokens.
It uses the useEffect hook to set up the PDF page rendering and event listeners for resizing and scrolling.
It renders the PDF page canvas, annotations, search results, and queued selections.
The Page component renders the following sub-components:
PageAnnotationsContainer: A styled container for the page annotations.
PageCanvas: A styled canvas element for rendering the PDF page.
Selection: Represents a single annotation selection on the page.
SearchResult: Represents a search result on the page.
The Page component handles mouse events for creating and modifying annotations:
On mouseDown, it initializes the selection if the necessary permissions are granted and the component is not in read-only mode.
On mouseMove, it updates the selection bounds if a selection is active.
On mouseUp, it adds the completed selection to the pageSelectionQueue and triggers the creation of a multi-page annotation if the Shift key is not pressed.
The Page component also handles fetching more annotations for previous and next pages using the FetchMoreOnVisible component.
The SelectionBoundary and SelectionTokens components are used to render the annotation boundaries and tokens, respectively.
The PDFPageRenderer class is responsible for rendering a single PDF page on the canvas. It manages the rendering tasks and provides methods for canceling and rescaling the rendering.
The getPageBoundsFromCanvas function calculates the bounding box of the page based on the canvas dimensions and its parent container.
Inside the useEffect hook that runs when the openedDocument prop changes, the PDF loading process is initiated.
The pdfjsLib.getDocument function from the pdfjs-dist library is used to load the PDF file specified by openedDocument.pdfFile.
The loading progress is tracked using the loadingTask.onProgress callback, which updates the progress state.
Once the PDF is loaded, the loadingTask.promise is resolved, and the PDFDocumentProxy object is obtained.
The PDFPageInfo objects are created for each page of the PDF using doc.getPage(i) and stored in the pages state.
Where and how are annotations loaded?
Annotations are loaded using the REQUEST_ANNOTATOR_DATA_FOR_DOCUMENT GraphQL query in the Annotator.tsx component.
The useQuery hook from Apollo Client is used to fetch the annotator data based on the provided initial_query_vars.
The annotator_data received from the query contains information about existing text annotations, document label annotations, and relationships.
The annotations are transformed into ServerAnnotation, DocTypeAnnotation, and RelationGroup objects and stored in the pdfAnnotations state using setPdfAnnotations.
Where is the PAWLs layer loaded?
The PAWLs layer is loaded in the Annotator.tsx component.
Inside the useEffect hook that runs when the openedDocument prop changes, the PAWLs layer is loaded using the getPawlsLayer function from api/rest.ts.
The getPawlsLayer function makes an HTTP GET request to fetch the PAWLs data file specified by openedDocument.pawlsParseFile.
The PAWLs data is expected to be an array of PageTokens objects, which contain token information for each page of the PDF.
The loaded PAWLs data is then used to create PDFPageInfo objects for each page, which include the page tokens.
The PDFView component is a top-level component that renders the PDF document with annotations, relations, and text search capabilities. It manages the state and functionality related to annotations, relations, and user interactions. Here's a detailed explanation of how the component works:
The PDFView component receives several props, including permissions, callbacks for CRUD operations on annotations and relations, refs for container and selection elements, and various configuration options.
It initializes several state variables using the useState hook, including:
selectionElementRefs and searchResultElementRefs: Refs for annotation selections and search results.
pageElementRefs: Refs for individual PDF pages.
scrollContainerRef: Ref for the scroll container.
textSearchMatches and searchText: State for text search matches and search text.
selectedAnnotations and selectedRelations: State for currently selected annotations and relations.
pageSelection and pageSelectionQueue: State for current page selection and queued selections.
pdfPageInfoObjs: State for PDF page information objects.
Various other state variables for active labels, relation modal visibility, and annotation options.
The component defines several functions for updating state and handling user interactions, such as:
insertSelectionElementRef, insertSearchResultElementRefs, and insertPageRef: Functions to add refs for selections, search results, and pages.
onError: Error handling callback.
advanceTextSearchMatch and reverseTextSearchMatch: Functions to navigate through text search matches.
onRelationModalOk and onRelationModalCancel: Callbacks for relation modal actions.
createMultiPageAnnotation: Function to create a multi-page annotation from queued selections.
The component uses the useEffect hook to handle side effects, such as:
Setting the scroll container ref on load.
Listening for changes in the shift key and triggering annotation creation.
Updating text search matches when the search text changes.
The component renders the PDF document and its related components using the PDFStore and AnnotationStore contexts:
The PDFStore context provides the PDF document, pages, and error handling.
The AnnotationStore context provides annotation-related state and functions.
The component renders the following main sections:
LabelSelector: Allows the user to select the active label for annotations.
DocTypeLabelDisplay: Displays the document type labels.
AnnotatorSidebar: Sidebar component for managing annotations and relations.
AnnotatorTopbar: Top bar component for additional controls and options.
PDF: The actual PDF component that renders the PDF pages and annotations.
The PDF component, defined in PDF.tsx, is responsible for rendering the PDF pages and annotations. It receives props from the PDFView component, such as permissions, configuration options, and callbacks.
The PDF component maps over each page of the PDF document and renders a Page component for each page, passing the necessary props.
The Page component, also defined in PDF.tsx, is responsible for rendering a single page of the PDF document along with its annotations and search results. It handles mouse events for creating and modifying annotations.
The PDFView component also renders the RelationModal component when the active relation label is set and the user has the necessary permissions. The modal allows the user to create or modify relations between annotations.
PDF renders the actual PDF document with annotations and text search capabilities. PDFView (see above) is what actually interacts with the backend / API.
The PDF component receives several props:
shiftDown: Indicates whether the Shift key is pressed (optional).
doc_permissions and corpus_permissions: Specify the permissions for the document and corpus, respectively.
read_only: Determines if the component is in read-only mode.
show_selected_annotation_only: Specifies whether to show only the selected annotation.
show_annotation_bounding_boxes: Specifies whether to show annotation bounding boxes.
show_annotation_labels: Specifies the behavior for displaying annotation labels.
setJumpedToAnnotationOnLoad: A callback function to set the jumped-to annotation on load.
The PDF component retrieves the PDF document and pages from the PDFStore context.
It maps over each page of the PDF document and renders a Page component for each page, passing the necessary props.
The Page component is responsible for rendering a single page of the PDF document along with its annotations and search results.
Inside the Page component:
It creates a canvas element using the useRef hook to render the PDF page.
It retrieves the annotations for the current page from the AnnotationStore context.
It defines a ConvertBoundsToSelections function that converts the selected bounds to annotations and tokens.
It uses the useEffect hook to set up the PDF page rendering and event listeners for resizing and scrolling.
It renders the PDF page canvas, annotations, search results, and queued selections.
The Page component renders the following sub-components:
PageAnnotationsContainer: A styled container for the page annotations.
PageCanvas: A styled canvas element for rendering the PDF page.
Selection: Represents a single annotation selection on the page.
SearchResult: Represents a search result on the page.
The Page component handles mouse events for creating and modifying annotations:
On mouseDown, it initializes the selection if the necessary permissions are granted and the component is not in read-only mode.
On mouseMove, it updates the selection bounds if a selection is active.
On mouseUp, it adds the completed selection to the pageSelectionQueue and triggers the creation of a multi-page annotation if the Shift key is not pressed.
The Page component also handles fetching more annotations for previous and next pages using the FetchMoreOnVisible component.
The SelectionBoundary and SelectionTokens components are used to render the annotation boundaries and tokens, respectively.
The PDFPageRenderer class is responsible for rendering a single PDF page on the canvas. It manages the rendering tasks and provides methods for canceling and rescaling the rendering.
The getPageBoundsFromCanvas function calculates the bounding box of the page based on the canvas dimensions and its parent container.
\ No newline at end of file
diff --git a/architecture/under-the-hood/index.html b/architecture/under-the-hood/index.html
index 0db8dd3f..6510bde3 100755
--- a/architecture/under-the-hood/index.html
+++ b/architecture/under-the-hood/index.html
@@ -7,6 +7,6 @@
.gdesc-inner { font-size: 0.75rem; }
body[data-md-color-scheme="slate"] .gdesc-inner { background: var(--md-default-bg-color);}
body[data-md-color-scheme="slate"] .gslide-title { color: var(--md-default-fg-color);}
- body[data-md-color-scheme="slate"] .gslide-desc { color: var(--md-default-fg-color);}
OpenContracts builds on the work that AllenAI did with PAWLs to create a consistent shared source of truth for data labeling and NLP algorithms, regardless of whether they are layout-aware, like LayoutLM or not, like BERT, Spacy or LexNLP. One of the challenges with natural language documents, particularly contracts is there are so many ways to structure any given file (e.g. .docx or .pdf) to represent exactly the same text. Even an identical document with identical formatting in a format like .pdf can have a significantly different file structure depending on what software was used to create it, the user's choices, and the software's own choices in deciding how to structure its output.
PAWLs and OpenContracts attempt to solve this by sending every document through a processing pipeline that provides a uniform and consistent way of extracting and structuring text and layout information. Using the parsing engine of Grobid and the open source OCR engine Tesseract, every single document is re-OCRed (to produce a consistent output for the same inputs) and then the "tokens" (text surrounded on all sides by whitespace - typically a word) in the OCRed document are stored as JSONs with their page and positional information. In OpenContracts, we refer to this JSON layer that combines text and positional data as the "PAWLs" layer. We use the PAWLs layer to build the full text extract from the document as well and store this as the "text layer".
Thus, in OpenContracts, every document has three files associated with it - the original pdf, a json file (the "PAWLs layer"), and a text file (the "text layer"). Because the text layer is built from the PAWLs layer, we can easily translate back and forth from text to positional information - e.g. given the start and end of a span of text the text layer, we can accurately say which PAWLs tokens the span includes, and, based on that, the x,y position of the span in the document.
This lets us take the outputs of many NLP libraries which typically produce only start and stop ranges and layer them perfectly on top of the original pdf. With the PAWLs tokens as the source of truth, we can seamlessly transition from text only to layout-aware text.
OCR is not perfect. By only accepting pdf inputs and OCRing every document, we do ignore any text embedded in the pdf. To the extent that text was exported accurately from whatever tool was used to write the document, this introduces some potential loss of fidelity - e.g. if you've ever seen an OCR engine mistake an 'O' or a 0 or 'I' for a '1' or something like that. Typically, however, the instance of such errors is fairly small, and it's a price we have to pay for the power of being able to effortlessly layer NLP outputs that have no layout awareness on top of complex, visual layouts.
OpenContracts builds on the work that AllenAI did with PAWLs to create a consistent shared source of truth for data labeling and NLP algorithms, regardless of whether they are layout-aware, like LayoutLM or not, like BERT, Spacy or LexNLP. One of the challenges with natural language documents, particularly contracts is there are so many ways to structure any given file (e.g. .docx or .pdf) to represent exactly the same text. Even an identical document with identical formatting in a format like .pdf can have a significantly different file structure depending on what software was used to create it, the user's choices, and the software's own choices in deciding how to structure its output.
PAWLs and OpenContracts attempt to solve this by sending every document through a processing pipeline that provides a uniform and consistent way of extracting and structuring text and layout information. Using the parsing engine of Grobid and the open source OCR engine Tesseract, every single document is re-OCRed (to produce a consistent output for the same inputs) and then the "tokens" (text surrounded on all sides by whitespace - typically a word) in the OCRed document are stored as JSONs with their page and positional information. In OpenContracts, we refer to this JSON layer that combines text and positional data as the "PAWLs" layer. We use the PAWLs layer to build the full text extract from the document as well and store this as the "text layer".
Thus, in OpenContracts, every document has three files associated with it - the original pdf, a json file (the "PAWLs layer"), and a text file (the "text layer"). Because the text layer is built from the PAWLs layer, we can easily translate back and forth from text to positional information - e.g. given the start and end of a span of text the text layer, we can accurately say which PAWLs tokens the span includes, and, based on that, the x,y position of the span in the document.
This lets us take the outputs of many NLP libraries which typically produce only start and stop ranges and layer them perfectly on top of the original pdf. With the PAWLs tokens as the source of truth, we can seamlessly transition from text only to layout-aware text.
OCR is not perfect. By only accepting pdf inputs and OCRing every document, we do ignore any text embedded in the pdf. To the extent that text was exported accurately from whatever tool was used to write the document, this introduces some potential loss of fidelity - e.g. if you've ever seen an OCR engine mistake an 'O' or a 0 or 'I' for a '1' or something like that. Typically, however, the instance of such errors is fairly small, and it's a price we have to pay for the power of being able to effortlessly layer NLP outputs that have no layout awareness on top of complex, visual layouts.
\ No newline at end of file
diff --git a/configuration/add-users/index.html b/configuration/add-users/index.html
index 255bc27e..18638d70 100755
--- a/configuration/add-users/index.html
+++ b/configuration/add-users/index.html
@@ -7,6 +7,6 @@
.gdesc-inner { font-size: 0.75rem; }
body[data-md-color-scheme="slate"] .gdesc-inner { background: var(--md-default-bg-color);}
body[data-md-color-scheme="slate"] .gslide-title { color: var(--md-default-fg-color);}
- body[data-md-color-scheme="slate"] .gslide-desc { color: var(--md-default-fg-color);}
You can use the same User admin page described above to create new users. Alternatively, go back to the main admin page http://localhost:8000/admin and, under the User section, click the "+Add" button:
Then, follow the on-screen instructions:
When you're done, the username and password you provided can be used to login.
OpenContracts is currently not built to allow users to self-register unless you use the Auth0 authentication. When managing users yourself, you'll need to add, remove and modify users via the admin panels.
You can use the same User admin page described above to create new users. Alternatively, go back to the main admin page http://localhost:8000/admin and, under the User section, click the "+Add" button:
Then, follow the on-screen instructions:
When you're done, the username and password you provided can be used to login.
OpenContracts is currently not built to allow users to self-register unless you use the Auth0 authentication. When managing users yourself, you'll need to add, remove and modify users via the admin panels.