diff --git a/.ipython/profile_default/history.sqlite b/.ipython/profile_default/history.sqlite index 1cbb6297..5b2d747a 100644 Binary files a/.ipython/profile_default/history.sqlite and b/.ipython/profile_default/history.sqlite differ diff --git a/config/graphql/graphene_types.py b/config/graphql/graphene_types.py index 87129af7..ed2cd496 100644 --- a/config/graphql/graphene_types.py +++ b/config/graphql/graphene_types.py @@ -120,6 +120,7 @@ class LabelTypeEnum(graphene.Enum): DOC_TYPE_LABEL = "DOC_TYPE_LABEL" TOKEN_LABEL = "TOKEN_LABEL" METADATA_LABEL = "METADATA_LABEL" + SPAN_LABEL = "SPAN_LABEL" class AnnotationSummaryType(graphene.ObjectType): @@ -412,7 +413,11 @@ def resolve_full_annotation_list(self, info, document_id=None): results = self.annotations.all() if document_id is not None: document_pk = from_global_id(document_id)[1] + logger.info( + f"Resolve full annotations for analysis {self.id} with doc {document_pk}" + ) results = results.filter(document_id=document_pk) + return results class Meta: @@ -429,6 +434,9 @@ class Meta: class FieldsetType(AnnotatePermissionsForReadMixin, DjangoObjectType): + in_use = graphene.Boolean( + description="True if the fieldset is used in any extract that has started." + ) full_column_list = graphene.List(ColumnType) class Meta: @@ -436,6 +444,12 @@ class Meta: interfaces = [relay.Node] connection_class = CountableConnection + def resolve_in_use(self, info) -> bool: + """ + Returns True if the fieldset is used in any extract that has started. + """ + return self.extracts.filter(started__isnull=False).exists() + def resolve_full_column_list(self, info): return self.columns.all() diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index 5611d700..d85a94de 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -74,9 +74,10 @@ make_corpus_public_task, ) from opencontractserver.types.dicts import OpenContractsAnnotatedDocumentImportType -from opencontractserver.types.enums import ExportType, PermissionTypes +from opencontractserver.types.enums import ExportType, LabelType, PermissionTypes from opencontractserver.users.models import UserExport from opencontractserver.utils.etl import is_dict_instance_of_typed_dict +from opencontractserver.utils.files import is_plaintext_content from opencontractserver.utils.permissioning import ( set_permissions_for_obj_to_user, user_has_permission_for_obj, @@ -814,6 +815,10 @@ class Arguments: description="If provided, successfully uploaded document will " "be uploaded to corpus with specified id", ) + add_to_extract_id = graphene.ID( + required=False, + description="If provided, successfully uploaded document will be added to extract with specified id", + ) make_public = graphene.Boolean( required=True, description="If True, document is immediately public. " @@ -835,7 +840,14 @@ def mutate( custom_meta, make_public, add_to_corpus_id=None, + add_to_extract_id=None, ): + if add_to_corpus_id is not None and add_to_extract_id is not None: + return UploadDocument( + message="Cannot simultaneously add document to both corpus and extract", + ok=False, + document=None, + ) ok = False document = None @@ -860,36 +872,75 @@ def mutate( # Check file type kind = filetype.guess(file_bytes) if kind is None: - return UploadDocument( - message="Unable to determine file type", ok=False, document=None - ) - if kind.mime not in settings.ALLOWED_DOCUMENT_MIMETYPES: + if is_plaintext_content(file_bytes): + kind = "application/txt" + else: + return UploadDocument( + message="Unable to determine file type", ok=False, document=None + ) + else: + kind = kind.mime + + if kind not in settings.ALLOWED_DOCUMENT_MIMETYPES: return UploadDocument( - message=f"Unallowed filetype: {kind.mime}", ok=False, document=None + message=f"Unallowed filetype: {kind}", ok=False, document=None ) user = info.context.user - pdf_file = ContentFile(file_bytes, name=filename) - document = Document( - creator=user, - title=title, - description=description, - custom_meta=custom_meta, - pdf_file=pdf_file, - backend_lock=True, - is_public=make_public, - ) - document.save() + + if kind in [ + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ]: + pdf_file = ContentFile(file_bytes, name=filename) + document = Document( + creator=user, + title=title, + description=description, + custom_meta=custom_meta, + pdf_file=pdf_file, + backend_lock=True, + is_public=make_public, + file_type=kind, # Store filetype + ) + document.save() + elif kind in ["application/txt"]: + txt_extract_file = ContentFile(file_bytes, name=filename) + document = Document( + creator=user, + title=title, + description=description, + custom_meta=custom_meta, + txt_extract_file=txt_extract_file, + backend_lock=True, + is_public=make_public, + file_type=kind, + ) + document.save() + set_permissions_for_obj_to_user(user, document, [PermissionTypes.CRUD]) - # If add_to_corpus_id is not None, link uploaded document to corpus + # Handle linking to corpus or extract if add_to_corpus_id is not None: try: corpus = Corpus.objects.get(id=from_global_id(add_to_corpus_id)[1]) transaction.on_commit(lambda: corpus.documents.add(document)) except Exception as e: message = f"Adding to corpus failed due to error: {e}" + elif add_to_extract_id is not None: + try: + extract = Extract.objects.get( + Q(pk=from_global_id(add_to_extract_id)[1]) + & (Q(creator=user) | Q(is_public=True)) + ) + if extract.finished is not None: + raise ValueError("Cannot add document to a finished extract") + transaction.on_commit(lambda: extract.documents.add(document)) + except Exception as e: + message = f"Adding to extract failed due to error: {e}" ok = True @@ -1063,13 +1114,24 @@ class Arguments: required=True, description="Id of the label that is applied via this annotation.", ) + annotation_type = graphene.Argument( + graphene.Enum.from_enum(LabelType), required=True + ) ok = graphene.Boolean() annotation = graphene.Field(AnnotationType) @login_required def mutate( - root, info, json, page, raw_text, corpus_id, document_id, annotation_label_id + root, + info, + json, + page, + raw_text, + corpus_id, + document_id, + annotation_label_id, + annotation_type, ): corpus_pk = from_global_id(corpus_id)[1] document_pk = from_global_id(document_id)[1] @@ -1085,6 +1147,7 @@ def mutate( annotation_label_id=label_pk, creator=user, json=json, + annotation_type=annotation_type.value, ) annotation.save() set_permissions_for_obj_to_user(user, annotation, [PermissionTypes.CRUD]) @@ -1924,20 +1987,95 @@ def mutate( return CreateExtract(ok=True, msg="SUCCESS!", obj=extract) -class UpdateExtractMutation(DRFMutation): - class IOSettings: - lookup_field = "id" - pk_fields = ["corpus", "fieldset", "creator"] - serializer = ExtractSerializer - model = Extract - graphene_model = ExtractType +class UpdateExtractMutation(graphene.Mutation): + """ + Mutation to update an existing Extract object. + Supports updating the name (title), corpus, fieldset, and error fields. + Ensures proper permission checks are applied. + """ class Arguments: - id = graphene.String(required=True) - title = graphene.String(required=False) - description = graphene.String(required=False) - icon = graphene.String(required=False) - label_set = graphene.String(required=False) + id = graphene.ID(required=True, description="ID of the Extract to update.") + title = graphene.String(required=False, description="New title for the Extract.") + corpus_id = graphene.ID(required=False, description="ID of the Corpus to associate with the Extract.") + fieldset_id = graphene.ID(required=False, description="ID of the Fieldset to associate with the Extract.") + error = graphene.String(required=False, description="Error message to update on the Extract.") + # The Extract model does not have 'description', 'icon', or 'label_set' fields. + # If these fields are added to the model, they can be included here. + + ok = graphene.Boolean() + message = graphene.String() + obj = graphene.Field(ExtractType) + + @staticmethod + @login_required + def mutate(root, info, id, title=None, corpus_id=None, fieldset_id=None, error=None): + print(f"UpdateExtractMutation.mutate called with: id={id}, title={title}, corpus_id={corpus_id}, fieldset_id={fieldset_id}, error={error}") + user = info.context.user + + try: + extract_pk = from_global_id(id)[1] + extract = Extract.objects.get(pk=extract_pk) + except Extract.DoesNotExist: + return UpdateExtractMutation(ok=False, message="Extract not found.", obj=None) + + # Check if the user has permission to update the Extract object + if not user_has_permission_for_obj( + user_val=user, + instance=extract, + permission=PermissionTypes.UPDATE, + include_group_permissions=True, + ): + return UpdateExtractMutation(ok=False, message="You don't have permission to update this extract.", obj=None) + + # Update fields + if title is not None: + extract.name = title + + if error is not None: + extract.error = error + + if corpus_id is not None: + corpus_pk = from_global_id(corpus_id)[1] + try: + corpus = Corpus.objects.get(pk=corpus_pk) + # Check permission + if not user_has_permission_for_obj( + user_val=user, + instance=corpus, + permission=PermissionTypes.READ, + include_group_permissions=True, + ): + return UpdateExtractMutation(ok=False, message="You don't have permission to use this corpus.", obj=None) + extract.corpus = corpus + except Corpus.DoesNotExist: + return UpdateExtractMutation(ok=False, message="Corpus not found.", obj=None) + + if fieldset_id is not None: + fieldset_pk = from_global_id(fieldset_id)[1] + print(f"Attempting to update extract {extract.id} with fieldset_id {fieldset_id} (pk: {fieldset_pk})") + try: + fieldset = Fieldset.objects.get(pk=fieldset_pk) + print(f"Found fieldset {fieldset.id} for update") + # Check permission + if not user_has_permission_for_obj( + user_val=user, + instance=fieldset, + permission=PermissionTypes.READ, + include_group_permissions=True, + ): + print(f"User {user.id} denied permission to use fieldset {fieldset.id}") + return UpdateExtractMutation(ok=False, message="You don't have permission to use this fieldset.", obj=None) + print(f"Updating extract {extract.id} fieldset to {fieldset.id}") + extract.fieldset = fieldset + except Fieldset.DoesNotExist: + print(f"Fieldset with pk {fieldset_pk} not found") + return UpdateExtractMutation(ok=False, message="Fieldset not found.", obj=None) + + extract.save() + extract.refresh_from_db() + + return UpdateExtractMutation(ok=True, message="Extract updated successfully.", obj=extract) class AddDocumentsToExtract(DRFMutation): diff --git a/config/graphql/queries.py b/config/graphql/queries.py index c181a885..5c39fa81 100644 --- a/config/graphql/queries.py +++ b/config/graphql/queries.py @@ -200,18 +200,22 @@ def resolve_annotations( # Filter by annotation_label__label_type logger.info( - f"Queryset county before filtering by annotation_label__label_type: {queryset.count()}" + f"Queryset count before filtering by annotation_label__label_type: {queryset.count()}" ) label_type = kwargs.get("annotation_label__label_type") if label_type: logger.info(f"Filtering by annotation_label__label_type: {label_type}") queryset = queryset.filter(annotation_label__label_type=label_type) + logger.info(f"Queryset count after filtering by label type: {queryset.count()}") - logger.info(f"QFilter value for analysis_isnull: {analysis_isnull}") + logger.info(f"Q Filter value for analysis_isnull: {analysis_isnull}") # Filter by analysis if analysis_isnull is not None: - logger.info(f"Filtering by analysis_isnull: {queryset.count()}") + logger.info( + f"QS count before filtering by analysis is null: {queryset.count()}" + ) queryset = queryset.filter(analysis__isnull=analysis_isnull) + logger.info(f"Filtered by analysis_isnull: {queryset.count()}") # Filter by document_id document_id = kwargs.get("document_id") diff --git a/config/settings/base.py b/config/settings/base.py index 4e63dbcb..bc53c2fd 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -133,7 +133,13 @@ # UPLOAD CONTROLS # ------------------------------------------------------------------------------ -ALLOWED_DOCUMENT_MIMETYPES = ["application/pdf"] +ALLOWED_DOCUMENT_MIMETYPES = [ + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/txt", +] # AUTHENTICATION # ------------------------------------------------------------------------------ diff --git a/docs/frontend/datagrid/rendering_custom_cells.md b/docs/frontend/datagrid/rendering_custom_cells.md new file mode 100644 index 00000000..c3a16b77 --- /dev/null +++ b/docs/frontend/datagrid/rendering_custom_cells.md @@ -0,0 +1,494 @@ +# Documentation: Implementing Custom Cell Renderers in React Data Grid + +This documentation provides a detailed explanation of how we implemented custom cell renderers using `react-data-grid` in our DataGrid. It also explains how to create additional custom renderers based on other properties of your data cells. + +## Introduction + +Custom cell rendering in `react-data-grid` allows you to define how each cell in your grid should be displayed. This is particularly useful when you need to display loading indicators, status icons, or any custom content based on the data cell's properties. + +In this guide, we'll walk through the steps of: + +- Creating custom cell renderers using the `renderCell` function in column definitions. +- Managing cell statuses to control what is displayed in each cell. +- Extending this approach to create more custom renderers based on other properties of your data cells. + +--- + +## Step 1: Understanding the `renderCell` Function in `react-data-grid` + +In `react-data-grid`, you can customize the rendering of cells by providing a `renderCell` function in your column definitions. This function receives cell properties and allows you to return a custom component or element to render. + +Here's the basic idea: + +```typescript +const columns = [ + { + key: 'columnKey', + name: 'Column Name', + renderCell: (props) => { + // Custom rendering logic + return ; + }, + }, + // ... other columns +]; +``` + +--- + +## Step 2: Creating a Custom Cell Renderer Component + +First, create a custom cell renderer component that will handle the rendering logic based on the cell's properties. + +### **File: `ExtractCellFormatter.tsx`** + +```typescript:src/components/extracts/datagrid/ExtractCellFormatter.tsx +import React from "react"; +import { Icon } from "semantic-ui-react"; + +export interface CellStatus { + isLoading: boolean; + isApproved: boolean; + isRejected: boolean; + isEdited: boolean; + originalData: any; + correctedData: any; + error?: any; +} + +interface ExtractCellFormatterProps { + value: string; + cellStatus: CellStatus; + onApprove: () => void; + onReject: () => void; + onEdit: () => void; +} + +/** + * ExtractCellFormatter is a custom cell renderer that displays the cell's value + * along with status icons based on the cell's status. + */ +export const ExtractCellFormatter: React.FC = ({ + value, + cellStatus, + onApprove, + onReject, + onEdit, +}) => { + const cellStyle: React.CSSProperties = { + display: "flex", + alignItems: "center", + width: "100%", + height: "100%", + padding: "0 8px", + }; + + if (cellStatus.isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {value} + {cellStatus.isApproved && ( + + )} + {cellStatus.isRejected && ( + + )} + {cellStatus.isEdited && ( + + )} +
+ ); +}; +``` + +**Explanation:** + +- The `ExtractCellFormatter` component accepts `value`, `cellStatus`, and action handlers as props. +- It displays a loading spinner if `cellStatus.isLoading` is `true`. +- Otherwise, it displays the cell's value and appropriate status icons based on `cellStatus`. + +--- + +## Step 3: Mapping Cell Statuses + +We need a way to determine the status of each cell to control what's displayed. We'll create a `cellStatusMap` that maps each cell to its status. + +### **Implementation** + +In your main `DataGrid` component file: + +```typescript:src/components/extracts/datagrid/DataGrid.tsx +import React, { useMemo } from "react"; +// ... other imports ... + +import { ExtractCellFormatter, CellStatus } from "./ExtractCellFormatter"; + +// ... other code ... + +/** + * ExtractDataGrid component displays the data grid with custom cell renderers. + */ +export const ExtractDataGrid: React.FC = ({ + extract, + cells, + rows, + columns, + // ... other props ... +}) => { + // ... existing state and effects ... + + /** + * Creates a map of cell statuses for quick access during rendering. + */ + const cellStatusMap = useMemo(() => { + const map = new Map(); + + cells.forEach((cell) => { + const extractIsProcessing = + extract.started && !extract.finished && !extract.error; + const cellIsProcessing = + cell.started && !cell.completed && !cell.failed; + const isProcessing = + cellIsProcessing || (extractIsProcessing && !cell.started); + + const status: CellStatus = { + isLoading: Boolean(isProcessing), + isApproved: Boolean(cell.approvedBy), + isRejected: Boolean(cell.rejectedBy), + isEdited: Boolean(cell.correctedData), + originalData: cell.data || null, + correctedData: cell.correctedData || null, + error: cell.failed || null, + }; + + const cellKey = `${cell.document.id}-${cell.column.id}`; + map.set(cellKey, status); + }); + + return map; + }, [cells, extract]); + + // ... other code ... +}; +``` + +**Explanation:** + +- The `cellStatusMap` uses a combination of `document.id` and `column.id` as the key to uniquely identify each cell. +- It stores the `CellStatus` for each cell, determining whether the cell is loading, approved, rejected, etc. + +--- + +## Step 4: Updating Column Definitions to Use `renderCell` + +Now, we'll update our column definitions to use the `renderCell` function, which utilizes our custom `ExtractCellFormatter`. + +### **Implementation** + +Still in your `DataGrid` component file: + +```typescript:src/components/extracts/datagrid/DataGrid.tsx +// ... other imports ... + +import DataGrid from "react-data-grid"; +import { ExtractCellFormatter, CellStatus } from "./ExtractCellFormatter"; + +// ... other code ... + +export const ExtractDataGrid: React.FC = (props) => { + // ... existing state and effects ... + + /** + * Defines the grid columns with custom renderers. + */ + const gridColumns = useMemo(() => { + return [ + // Static column for document titles + { + key: "documentTitle", + name: "Document", + frozen: true, + width: 200, + renderCell: (props: any) => { + return
{props.row.documentTitle}
; + }, + }, + // Dynamic columns based on `columns` + ...columns.map((col) => ({ + key: col.id, + name: col.name, + width: 250, + /** + * Custom cell rendering for each column. + */ + renderCell: (props: any) => { + const cellKey = `${props.row.documentId}-${col.id}`; + const cellStatus = cellStatusMap.get(cellKey) || { + isLoading: true, + isApproved: false, + isRejected: false, + isEdited: false, + originalData: null, + correctedData: null, + error: null, + }; + + const value = props.row[col.id] || ""; + + return ( + { + // Implement approval logic + }} + onReject={() => { + // Implement rejection logic + }} + onEdit={() => { + // Implement edit logic + }} + /> + ); + }, + })), + ]; + }, [columns, cellStatusMap]); + + // ... existing code to generate gridRows ... + + return ( + + ); +}; +``` + +**Explanation:** + +- For each column, we define a `renderCell` function. +- In `renderCell`, we: + - Determine the `cellKey` to retrieve the cell's status. + - Retrieve the `cellStatus` from `cellStatusMap`. If not found, we assume the cell is loading. + - Fetch the cell's value from `props.row`. + - Render the `ExtractCellFormatter`, passing in the necessary props. + +--- + +## Step 5: Implementing Additional Custom Renderers + +To create more custom renderers based on other properties of your data cells, you can follow a similar pattern. Here's how you can do it: + +### **1. Define Additional Cell Status Properties** + +Extend your `CellStatus` interface to include any new properties you need. + +```typescript:src/components/extracts/datagrid/ExtractCellFormatter.tsx +export interface CellStatus { + isLoading: boolean; + isApproved: boolean; + isRejected: boolean; + isEdited: boolean; + hasComment: boolean; // New property + needsReview: boolean; // New property + originalData: any; + correctedData: any; + error?: any; +} +``` + +### **2. Update `cellStatusMap` to Include New Properties** + +Modify the mapping logic to set these new properties based on your data. + +```typescript:src/components/extracts/datagrid/DataGrid.tsx +const cellStatusMap = useMemo(() => { + const map = new Map(); + + cells.forEach((cell) => { + // ... existing logic ... + + const status: CellStatus = { + // ... existing properties ... + hasComment: Boolean(cell.comment), + needsReview: Boolean(cell.needsReview), + }; + + // ... existing logic ... + }); + + return map; +}, [cells, extract]); +``` + +### **3. Update the Custom Cell Renderer to Handle New Properties** + +Modify your `ExtractCellFormatter` to display additional icons or elements based on the new status properties. + +```typescript:src/components/extracts/datagrid/ExtractCellFormatter.tsx +export const ExtractCellFormatter: React.FC = ({ + value, + cellStatus, + onApprove, + onReject, + onEdit, +}) => { + // ... existing code ... + + return ( +
+ {value} + {cellStatus.hasComment && ( + + )} + {cellStatus.needsReview && ( + + )} + {/* Existing status icons */} + {cellStatus.isApproved && ( + + )} + {cellStatus.isRejected && ( + + )} + {cellStatus.isEdited && ( + + )} +
+ ); +}; +``` + +**Explanation:** + +- The new properties `hasComment` and `needsReview` control whether additional icons are displayed. +- You can add any number of new properties and correspondingly update the rendering logic. + +### **4. Create Completely New Custom Renderers** + +If you need entirely different rendering logic for certain columns or cells, you can create new components. + +#### **Example: A Date Formatter** + +Suppose you have a column that displays dates, and you want to format them in a specific way. + +**Create the Custom Renderer:** + +```typescript:src/components/extracts/datagrid/DateCellFormatter.tsx +import React from "react"; +import { format } from "date-fns"; + +interface DateCellFormatterProps { + value: string; +} + +/** + * DateCellFormatter formats a date string into a readable format. + */ +export const DateCellFormatter: React.FC = ({ value }) => { + const date = new Date(value); + const formattedDate = format(date, "yyyy-MM-dd"); // Customize the format as needed + + return
{formattedDate}
; +}; +``` + +**Update the Column Definition:** + +```typescript:src/components/extracts/datagrid/DataGrid.tsx +// ... imports ... + +import { DateCellFormatter } from "./DateCellFormatter"; + +// ... other code ... + +const gridColumns = useMemo(() => { + return [ + // ... other columns ... + { + key: "dateColumn", + name: "Date", + width: 150, + renderCell: (props: any) => { + const value = props.row["dateColumn"]; + return ; + }, + }, + // ... other columns ... + ]; +}, [/* dependencies */]); +``` + +--- + +## Step 6: Additional Considerations + +### **Type Safety** + +Ensure that all your components and functions are correctly typed. This helps catch errors during development. + +- Use `interface` or `type` definitions for your props and state variables. +- Type your functions and hooks appropriately. + +### **Action Handlers** + +Implement the action handlers passed to your cell renderers (`onApprove`, `onReject`, `onEdit`, etc.). + +```typescript +const handleApprove = (cellKey: string) => { + // Implement approval logic +}; + +const handleReject = (cellKey: string) => { + // Implement rejection logic +}; + +const handleEdit = (cellKey: string) => { + // Implement edit logic +}; +``` + +Pass these handlers to your `ExtractCellFormatter`: + +```typescript +renderCell: (props: any) => { + const cellKey = `${props.row.documentId}-${col.id}`; + // ... existing code ... + + return ( + handleApprove(cellKey)} + onReject={() => handleReject(cellKey)} + onEdit={() => handleEdit(cellKey)} + /> + ); +}, +``` + +### **Styling** + +Customize the styling of your cells and components using CSS or inline styles. + +--- + +## Conclusion + +By following these steps, you can implement custom cell renderers in `react-data-grid` that display loading indicators and status icons based on the properties of your data cells. You can extend this approach to create additional custom renderers tailored to your specific requirements. + +**Key Takeaways:** + +- Use the `renderCell` function in column definitions to customize cell rendering. +- Maintain a `cellStatusMap` or similar structure to manage the state and status of each cell. +- Create reusable components for different types of cell renderers. +- Ensure type safety and include docstrings for better maintainability. diff --git a/fixtures/vcr_cassettes/run_query.yaml b/fixtures/vcr_cassettes/run_query.yaml index ae066835..ab9f9987 100644 --- a/fixtures/vcr_cassettes/run_query.yaml +++ b/fixtures/vcr_cassettes/run_query.yaml @@ -9,9 +9,9 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - c42050e0-2aba-4e6c-b3ff-d846ac65f466 + - f39506f5-7547-46a8-99f8-74e348d73655 user-agent: - - sentence-transformers/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1 + - sentence-transformers/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1 method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/modules.json response: @@ -31,11 +31,11 @@ interactions: Content-Length: - '349' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:15 GMT + - Thu, 17 Oct 2024 05:14:37 GMT ETag: - '"952a9b81c0bfd99800fabf352f69c7ccd46c5e43"' Referrer-Policy: @@ -43,19 +43,19 @@ interactions: Vary: - Origin Via: - - 1.1 01fc452cc2ba7a24791ff6a3ac1b51e0.cloudfront.net (CloudFront) + - 1.1 50e5d5267caad4bbba42e8e11cdd9960.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - DmJCIsy5nVsq5g6RUsf2fAJgt8Ss3ioNdkT8uXvk9XseSWHgLb-yuw== + - -fxar55m1GmtX-OFsbKhLYlTN9g4z8IY7BuiOJ0ysqCjRStcfh4wLQ== X-Amz-Cf-Pop: - - DFW56-P3 + - IAH50-C3 X-Cache: - Miss from cloudfront X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccef-021d6b0735f82c096e30ae2d;c42050e0-2aba-4e6c-b3ff-d846ac65f466 + - Root=1-67109d3d-1d06d09032fa68443ebe952f;f39506f5-7547-46a8-99f8-74e348d73655 cross-origin-opener-policy: - same-origin status: @@ -71,9 +71,9 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - 2dd18b00-eb2f-4e67-868c-69fb8a39986f + - 8b2bcede-2a4d-4089-b6d9-73addbe265b4 user-agent: - - sentence-transformers/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1 + - sentence-transformers/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1 method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/config_sentence_transformers.json response: @@ -93,11 +93,11 @@ interactions: Content-Length: - '116' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:15 GMT + - Thu, 17 Oct 2024 05:14:37 GMT ETag: - '"fd1b291129c607e5d49799f87cb219b27f98acdf"' Referrer-Policy: @@ -105,19 +105,19 @@ interactions: Vary: - Origin Via: - - 1.1 8b40d2318598b57f3a4cb29b2f0fb820.cloudfront.net (CloudFront) + - 1.1 8ec3b7b16a00323f1c24e1de0379c34a.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - W30PYweGPDJrpZ5Hv4wHXuw8CfK5PJ7X5qnZAk3-hMErDYCBpIFs_g== + - ySA4wwcTDEiUrcLuZpKWzTypxHbrqteNUnFqDmD4nQEyyuIiW7LFRg== X-Amz-Cf-Pop: - - DFW56-P3 + - IAH50-C3 X-Cache: - Miss from cloudfront X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccef-5c719fd83e8099237741396a;2dd18b00-eb2f-4e67-868c-69fb8a39986f + - Root=1-67109d3d-42645fb46a73354c6e9169f0;8b2bcede-2a4d-4089-b6d9-73addbe265b4 cross-origin-opener-policy: - same-origin status: @@ -133,9 +133,9 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - 52e0b5c0-0e19-42ea-8219-87d15b82bccd + - 8603fa6d-079b-4054-8435-10fd06f846af user-agent: - - sentence-transformers/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1 + - sentence-transformers/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1 method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/README.md response: @@ -155,11 +155,11 @@ interactions: Content-Length: - '11586' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:15 GMT + - Thu, 17 Oct 2024 05:14:37 GMT ETag: - '"49291b15aecddcea5f99be93dde87cbfdfb3127b"' Referrer-Policy: @@ -167,19 +167,19 @@ interactions: Vary: - Origin Via: - - 1.1 e6a210d32373f8c8e5c59660a5ef51d8.cloudfront.net (CloudFront) + - 1.1 e128a559fe04157544ff370390660ea0.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - 4A54PMCjuvTChEV4QV7BR8i86QpiXg2rLVyO5umJP6RoHEbdqtU0Sg== + - NaTOLSKXFTDljJJbDUaS2pidXrpbf7AGFo2di-QQqLlhruw_cDFhCg== X-Amz-Cf-Pop: - - DFW56-P3 + - IAH50-C3 X-Cache: - Miss from cloudfront X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccef-7dce73f77a57082a1dc25d40;52e0b5c0-0e19-42ea-8219-87d15b82bccd + - Root=1-67109d3d-195defda30fb801c58c61084;8603fa6d-079b-4054-8435-10fd06f846af cross-origin-opener-policy: - same-origin status: @@ -195,9 +195,9 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - f09d0367-174c-4157-ad13-311fad36f201 + - 49e23f2b-e3f5-4660-bc48-515df8e4c111 user-agent: - - sentence-transformers/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1 + - sentence-transformers/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1 method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/modules.json response: @@ -217,11 +217,11 @@ interactions: Content-Length: - '349' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:16 GMT + - Thu, 17 Oct 2024 05:14:37 GMT ETag: - '"952a9b81c0bfd99800fabf352f69c7ccd46c5e43"' Referrer-Policy: @@ -229,19 +229,19 @@ interactions: Vary: - Origin Via: - - 1.1 5c2c969e1efb957f3541c48cdf2f9d6a.cloudfront.net (CloudFront) + - 1.1 b60e1b44fca3d5bb9d63dcd0fb1737aa.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - pGnu0_VjrnY51Ut3bx16lIhQk7SXTEDO7JCp_buRP1dZqCYDK7fjyg== + - kECVM7ovwomTA_KmOn5odbR_QkTAyAoSu3SD52_AXBUNgwllpAqFDg== X-Amz-Cf-Pop: - - DFW56-P3 + - IAH50-C3 X-Cache: - Miss from cloudfront X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccef-28691392080780163a8d2956;f09d0367-174c-4157-ad13-311fad36f201 + - Root=1-67109d3d-137b549a228c55350d99ae54;49e23f2b-e3f5-4660-bc48-515df8e4c111 cross-origin-opener-policy: - same-origin status: @@ -257,9 +257,9 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - b3a5682a-b1ed-45fa-9e8c-0db7f6b2474d + - 3fe84757-8583-40dd-a1eb-70e281c6bedb user-agent: - - sentence-transformers/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1 + - sentence-transformers/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1 method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/sentence_bert_config.json response: @@ -279,11 +279,11 @@ interactions: Content-Length: - '53' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:16 GMT + - Thu, 17 Oct 2024 05:14:37 GMT ETag: - '"f789d99277496b282d19020415c5ba9ca79ac875"' Referrer-Policy: @@ -291,19 +291,19 @@ interactions: Vary: - Origin Via: - - 1.1 93e20f04c6918da9c97f5798c5d3e818.cloudfront.net (CloudFront) + - 1.1 49b3b3bca8c1a893d1cd36619612ed04.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - geCj4tpUK05iNFMDMvcBr77JbF9RbkpQfSmWszq6coRkPRElRrS9UQ== + - SB8BEDTOZThgNnL30zsLuHWljFZcipCv8K-5VIM-vSbBeTlEFvfOGg== X-Amz-Cf-Pop: - - DFW56-P3 + - IAH50-C3 X-Cache: - Miss from cloudfront X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccf0-3c2ea1da6740be7c33a130f2;b3a5682a-b1ed-45fa-9e8c-0db7f6b2474d + - Root=1-67109d3d-563217573c0fe18f1b490874;3fe84757-8583-40dd-a1eb-70e281c6bedb cross-origin-opener-policy: - same-origin status: @@ -319,10 +319,10 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - 70f0b99f-6a86-4335-90cf-7e93958cfbf6 + - 3e272ac0-943f-431b-9f37-5718e7c7f685 user-agent: - - unknown/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1; transformers/4.41.2; - session_id/c5e102d9de1b472dbff4798a7adece4e; file_type/config; from_auto_class/True + - unknown/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1; transformers/4.45.2; + session_id/309083ddf9764ff1944a6552859b6908; file_type/config; from_auto_class/True method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/config.json response: @@ -342,11 +342,11 @@ interactions: Content-Length: - '612' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:16 GMT + - Thu, 17 Oct 2024 05:14:38 GMT ETag: - '"72b987fd805cfa2b58c4c8c952b274a11bfd5a00"' Referrer-Policy: @@ -354,19 +354,19 @@ interactions: Vary: - Origin Via: - - 1.1 77bbaa12f1db74ae58b5588b8dc2ac98.cloudfront.net (CloudFront) + - 1.1 2bdf494b25915e360d3b11ea33e35b3a.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - sOWW-mOSAdGNN7a7cnNL5nGGAUpfaGbpeGTubMc2miil3TvWKcnTpQ== + - j2b-BCAXfveY2DhmdRDxf7PmA2gZMgTa5Z0B_Ev79H18dPWu3c8x4A== X-Amz-Cf-Pop: - - DFW56-P3 + - IAH50-C3 X-Cache: - Miss from cloudfront X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccf0-5e3b8e8a5628d74b4206988a;70f0b99f-6a86-4335-90cf-7e93958cfbf6 + - Root=1-67109d3e-384390b82081389a11ae7e0a;3e272ac0-943f-431b-9f37-5718e7c7f685 cross-origin-opener-policy: - same-origin status: @@ -382,10 +382,10 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - 2be312a3-cd7a-4910-999e-da6a826f0371 + - c8a64b7b-5094-4876-9f72-751e032cbfb6 user-agent: - - unknown/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1; transformers/4.41.2; - session_id/c5e102d9de1b472dbff4798a7adece4e + - unknown/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1; transformers/4.45.2; + session_id/309083ddf9764ff1944a6552859b6908 method: HEAD uri: https://huggingface.co/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/resolve/main/tokenizer_config.json response: @@ -405,11 +405,11 @@ interactions: Content-Length: - '383' Content-Security-Policy: - - default-src none; sandbox + - default-src 'none'; sandbox Content-Type: - text/plain; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:17 GMT + - Thu, 17 Oct 2024 05:14:39 GMT ETag: - '"d50701956f8e35e7507e6b74547a71e62ca39cad"' Referrer-Policy: @@ -417,9 +417,9 @@ interactions: Vary: - Origin Via: - - 1.1 8cfa3ac64c5335f6622a388421fb1f1a.cloudfront.net (CloudFront) + - 1.1 8b40d2318598b57f3a4cb29b2f0fb820.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - tWdw4Xr_cV5CTKXC3qiorxAyQaBr-bnIUH9w4sI-49JQbco_XHB8qQ== + - 4mEgcYxY7r_uPAsnf7uitOwkCDtCsvtIoZw1xFGWF5g6un3Br0QRUg== X-Amz-Cf-Pop: - DFW56-P3 X-Cache: @@ -427,9 +427,9 @@ interactions: X-Powered-By: - huggingface-moon X-Repo-Commit: - - 2430568290bb832d22ad5064f44dd86cf0240142 + - 2d981ed0b0b8591b038d472b10c38b96016aab2e X-Request-Id: - - Root=1-6673ccf1-6deb521614d70597234511d0;2be312a3-cd7a-4910-999e-da6a826f0371 + - Root=1-67109d3f-60d3e5f1335458cb7f756261;c8a64b7b-5094-4876-9f72-751e032cbfb6 cross-origin-opener-policy: - same-origin status: @@ -445,16 +445,16 @@ interactions: Connection: - keep-alive X-Amzn-Trace-Id: - - 82c7248c-99ec-4ac9-9e46-d1253d5392ac + - ea82ccbe-de08-4e8c-bf0b-ca5ed28ec031 user-agent: - - sentence-transformers/None; hf_hub/0.23.4; python/3.10.12; torch/2.3.1 + - sentence-transformers/None; hf_hub/0.25.2; python/3.10.12; torch/2.4.1 method: GET uri: https://huggingface.co/api/models/sentence-transformers/multi-qa-MiniLM-L6-cos-v1/revision/main response: body: - string: '{"_id":"621ffdc136468d709f1802cf","id":"sentence-transformers/multi-qa-MiniLM-L6-cos-v1","modelId":"sentence-transformers/multi-qa-MiniLM-L6-cos-v1","author":"sentence-transformers","sha":"2430568290bb832d22ad5064f44dd86cf0240142","lastModified":"2024-03-27T11:37:43.000Z","private":false,"disabled":false,"gated":false,"pipeline_tag":"sentence-similarity","tags":["sentence-transformers","pytorch","tf","safetensors","bert","feature-extraction","sentence-similarity","transformers","en","dataset:flax-sentence-embeddings/stackexchange_xml","dataset:ms_marco","dataset:gooaq","dataset:yahoo_answers_topics","dataset:search_qa","dataset:eli5","dataset:natural_questions","dataset:trivia_qa","dataset:embedding-data/QQP","dataset:embedding-data/PAQ_pairs","dataset:embedding-data/Amazon-QA","dataset:embedding-data/WikiAnswers","autotrain_compatible","endpoints_compatible","text-embeddings-inference","region:us"],"downloads":1969565,"library_name":"sentence-transformers","mask_token":"[MASK]","widgetData":[{"source_sentence":"That + string: '{"_id":"621ffdc136468d709f1802cf","id":"sentence-transformers/multi-qa-MiniLM-L6-cos-v1","modelId":"sentence-transformers/multi-qa-MiniLM-L6-cos-v1","author":"sentence-transformers","sha":"2d981ed0b0b8591b038d472b10c38b96016aab2e","lastModified":"2024-10-11T13:42:04.000Z","private":false,"disabled":false,"gated":false,"pipeline_tag":"sentence-similarity","tags":["sentence-transformers","pytorch","tf","onnx","safetensors","openvino","bert","feature-extraction","sentence-similarity","transformers","en","dataset:flax-sentence-embeddings/stackexchange_xml","dataset:ms_marco","dataset:gooaq","dataset:yahoo_answers_topics","dataset:search_qa","dataset:eli5","dataset:natural_questions","dataset:trivia_qa","dataset:embedding-data/QQP","dataset:embedding-data/PAQ_pairs","dataset:embedding-data/Amazon-QA","dataset:embedding-data/WikiAnswers","autotrain_compatible","text-embeddings-inference","endpoints_compatible","region:us"],"downloads":2092795,"library_name":"sentence-transformers","mask_token":"[MASK]","widgetData":[{"source_sentence":"That is a happy person","sentences":["That is a happy dog","That is a very happy - person","Today is a sunny day"]}],"likes":104,"model-index":null,"config":{"architectures":["BertModel"],"model_type":"bert","tokenizer_config":{"unk_token":"[UNK]","sep_token":"[SEP]","pad_token":"[PAD]","cls_token":"[CLS]","mask_token":"[MASK]"}},"cardData":{"language":["en"],"library_name":"sentence-transformers","tags":["sentence-transformers","feature-extraction","sentence-similarity","transformers"],"datasets":["flax-sentence-embeddings/stackexchange_xml","ms_marco","gooaq","yahoo_answers_topics","search_qa","eli5","natural_questions","trivia_qa","embedding-data/QQP","embedding-data/PAQ_pairs","embedding-data/Amazon-QA","embedding-data/WikiAnswers"],"pipeline_tag":"sentence-similarity"},"transformersInfo":{"auto_model":"AutoModel","pipeline_tag":"feature-extraction","processor":"AutoTokenizer"},"siblings":[{"rfilename":".gitattributes"},{"rfilename":"1_Pooling/config.json"},{"rfilename":"README.md"},{"rfilename":"config.json"},{"rfilename":"config_sentence_transformers.json"},{"rfilename":"data_config.json"},{"rfilename":"model.safetensors"},{"rfilename":"modules.json"},{"rfilename":"pytorch_model.bin"},{"rfilename":"sentence_bert_config.json"},{"rfilename":"special_tokens_map.json"},{"rfilename":"tf_model.h5"},{"rfilename":"tokenizer.json"},{"rfilename":"tokenizer_config.json"},{"rfilename":"train_script.py"},{"rfilename":"vocab.txt"}],"spaces":["jorge-henao/ask2democracycol","Abhilashvj/haystack_QA","Sukhyun/course_recommender","namoopsoo/text-audio-video","somosnlp-hackathon-2023/ask2democracy","Waflon/FAQ_SSI_CHILE","mnemlaghi/canap","PBusienei/Summit_app_demo","myshirk/semantic-search-datasets","namoopsoo/text-audio-video-policies","namoopsoo/text-audio-video-policies-t4","gfhayworth/chat_qa_demo","simplexico/legal-ai-actions","Warlord-K/IITI-Similarity","Warlord-K/LatestFashion","brka-ot/sentence-transformers-multi-qa-MiniLM-L6-cos-v1-feature-extraction","Slycat/Southampton-Similarity","eaglelandsonce/RAG-with-Phi-2-and-LangChain","rishabincloud/RAG-Phi2-LangChain","QaillcNextGen/RAG-with-Phi-2-and-LangChain","yalaa/simplewiki-retrieve-rerank","ravi259/baserag_hf","Lakshita336/bot"],"createdAt":"2022-03-02T23:29:05.000Z","safetensors":{"parameters":{"I64":512,"F32":22713216},"total":22713728}}' + person","Today is a sunny day"]}],"likes":115,"model-index":null,"config":{"architectures":["BertModel"],"model_type":"bert","tokenizer_config":{"unk_token":"[UNK]","sep_token":"[SEP]","pad_token":"[PAD]","cls_token":"[CLS]","mask_token":"[MASK]"}},"cardData":{"language":["en"],"library_name":"sentence-transformers","tags":["sentence-transformers","feature-extraction","sentence-similarity","transformers"],"datasets":["flax-sentence-embeddings/stackexchange_xml","ms_marco","gooaq","yahoo_answers_topics","search_qa","eli5","natural_questions","trivia_qa","embedding-data/QQP","embedding-data/PAQ_pairs","embedding-data/Amazon-QA","embedding-data/WikiAnswers"],"pipeline_tag":"sentence-similarity"},"transformersInfo":{"auto_model":"AutoModel","pipeline_tag":"feature-extraction","processor":"AutoTokenizer"},"siblings":[{"rfilename":".gitattributes"},{"rfilename":"1_Pooling/config.json"},{"rfilename":"README.md"},{"rfilename":"config.json"},{"rfilename":"config_sentence_transformers.json"},{"rfilename":"data_config.json"},{"rfilename":"model.safetensors"},{"rfilename":"modules.json"},{"rfilename":"onnx/model.onnx"},{"rfilename":"onnx/model_O1.onnx"},{"rfilename":"onnx/model_O2.onnx"},{"rfilename":"onnx/model_O3.onnx"},{"rfilename":"onnx/model_O4.onnx"},{"rfilename":"onnx/model_qint8_arm64.onnx"},{"rfilename":"onnx/model_qint8_avx512.onnx"},{"rfilename":"onnx/model_qint8_avx512_vnni.onnx"},{"rfilename":"onnx/model_quint8_avx2.onnx"},{"rfilename":"openvino/openvino_model.bin"},{"rfilename":"openvino/openvino_model.xml"},{"rfilename":"pytorch_model.bin"},{"rfilename":"sentence_bert_config.json"},{"rfilename":"special_tokens_map.json"},{"rfilename":"tf_model.h5"},{"rfilename":"tokenizer.json"},{"rfilename":"tokenizer_config.json"},{"rfilename":"train_script.py"},{"rfilename":"vocab.txt"}],"spaces":["mteb/leaderboard","jorge-henao/ask2democracycol","Abhilashvj/haystack_QA","namoopsoo/text-audio-video","Sukhyun/course_recommender","somosnlp-hackathon-2023/ask2democracy","Waflon/FAQ_SSI_CHILE","mnemlaghi/canap","PBusienei/Summit_app_demo","myshirk/semantic-search-datasets","namoopsoo/text-audio-video-policies","namoopsoo/text-audio-video-policies-t4","gfhayworth/chat_qa_demo","simplexico/legal-ai-actions","Warlord-K/IITI-Similarity","Warlord-K/LatestFashion","brka-ot/sentence-transformers-multi-qa-MiniLM-L6-cos-v1-feature-extraction","eaglelandsonce/RAG-with-Phi-2-and-LangChain","rishabincloud/RAG-Phi2-LangChain","QaillcNextGen/RAG-with-Phi-2-and-LangChain","yalaa/simplewiki-retrieve-rerank","Slycat/Southampton-Similarity","ravi259/baserag_hf","Lakshita336/bot","Vadim212/test2","Ankitajadhav/Whats_Cooking","marcid/sentence-transformers-multi-qa-MiniLM-L6-cos-v1","aipoc/TemplateComparizer","brichett/tsgpt","aipoc/AiContract"],"createdAt":"2022-03-02T23:29:05.000Z","safetensors":{"parameters":{"I64":512,"F32":22713216},"total":22713728}}' headers: Access-Control-Allow-Origin: - https://huggingface.co @@ -463,21 +463,21 @@ interactions: Connection: - keep-alive Content-Length: - - '3353' + - '3999' Content-Type: - application/json; charset=utf-8 Date: - - Thu, 20 Jun 2024 06:32:18 GMT + - Thu, 17 Oct 2024 05:14:39 GMT ETag: - - W/"d19-bcnRJ0pa6T1Bg56DtVbeCHFnT6E" + - W/"f9f-mYMHCjrYd8Qg53O/ZnWm/yiYDvM" Referrer-Policy: - strict-origin-when-cross-origin Vary: - Origin Via: - - 1.1 9f39a08ea59fcf3ed020e2cd341ac082.cloudfront.net (CloudFront) + - 1.1 8cfa3ac64c5335f6622a388421fb1f1a.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - xxEjVrgpgyctn-ed00_RU92YLG00VGPgEDn64h1VAQ7QYjZjYzUoig== + - 70cHd3tL1cEAWEqelVr2dnbvv1R65k9asfJxcFCA0E51ORBBSkoVnQ== X-Amz-Cf-Pop: - DFW56-P3 X-Cache: @@ -485,7 +485,7 @@ interactions: X-Powered-By: - huggingface-moon X-Request-Id: - - Root=1-6673ccf2-5f19bfee1cbd02f2566a0c1b;82c7248c-99ec-4ac9-9e46-d1253d5392ac + - Root=1-67109d3f-1c5a75b04b96da877071143b;ea82ccbe-de08-4e8c-bf0b-ca5ed28ec031 cross-origin-opener-policy: - same-origin status: @@ -513,8 +513,6 @@ interactions: - application/json accept-encoding: - gzip, deflate - authorization: - - Bearer sk-opencontracts-testing-UHD5Qk7d8lwnYsAjbsgLT3BlbkFJWvrzpMWnxi7ZoO6m4knz connection: - keep-alive content-length: @@ -524,7 +522,7 @@ interactions: host: - api.openai.com user-agent: - - OpenAI/Python 1.34.0 + - OpenAI/Python 1.51.2 x-stainless-arch: - x64 x-stainless-async: @@ -534,7 +532,9 @@ interactions: x-stainless-os: - Linux x-stainless-package-version: - - 1.34.0 + - 1.51.2 + x-stainless-retry-count: + - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: @@ -544,18 +544,18 @@ interactions: response: body: string: !!binary | - H4sIAAAAAAAAA1SQQU/DMAyF7/0VVs4rWtsV1t2QOCEBtwmB0JSlbhdI7ZB4A4T231G6scElBz9/ - L+/5OwNQtlULUGajxQze5Y2p25u+Xjay/Bzmpriip1t+WN6tr5v5o5okgtevaOSXujA8eIdimQ6y - CagFk2txVcznl3VRNaMwcIsuYb2XfMZ5OS1n+bTOi+oIbtgajGoBzxkAwPf4pojU4qdawHTyOxkw - Rt2jWpyWAFRglyZKx2ijaBI1OYuGSZDG1PdMCNyBbBB84J1tsYXI22AwQtrTlsBSx2HQqRYEdLjT - JCAMmuIHBkv9iL9vMXxdqOM3+1M+x70PvE5daOvcad5ZsnGzCqgjU8oShf0B32cAL+Mdtv+qKR94 - 8LISfkNKhlVZHvzU+fJntaiOorBo94eq6uyYUMWvKDisOks9Bh/s4SydX3UzvCybdqprle2zHwAA - AP//AwDgrnwrHwIAAA== + H4sIAAAAAAAAA2xRy07DMBC85ytWPjcoTSNoe0OiUgEJiQMXEIqMs0lMHa+xN1UR6r8jp09ELz7M + 7Ixndn8SAKErMQehWsmqcya9fbibPtjpcqGep4vJsv962cyMui/Uyjy+ilFU0McnKj6orhR1ziBr + sjtaeZSM0XV8k8/GRZ7dzAaiowpNlDWO04LSPMuLNJum2fVe2JJWGMQc3hIAgJ/hjRFthRsxh2x0 + QDoMQTYo5schAOHJRETIEHRgaVmMTqQiy2iH1E9kEagGbhEC9V5hAOdprSusIM5JbUHbmnwnYy3w + aHAtLQPTIPrq0X9fnbt7rPsgYznbG7PHt8e4hhrn6SPs+SNea6tDW3qUgWyMFpicGNhtAvA+rKX/ + 01Q4T53jkmmFNhpO8nznJ06HOLHjA8nE0pypJsXogl9ZIUttwtlihZKqxeokzZKzcv8/vWSxK6ht + 888l2TuJ8B0Yu7LWtkHvvN6dqnalzDNZIE6KQiTb5BcAAP//AwDxC4IZswIAAA== headers: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8969b8922904eb1f-DFW + - 8d3dce6d89ef45f6-DFW Connection: - keep-alive Content-Encoding: @@ -563,41 +563,45 @@ interactions: Content-Type: - application/json Date: - - Thu, 20 Jun 2024 06:32:19 GMT + - Thu, 17 Oct 2024 05:14:40 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=2DnYPG47fdcZ4pM0n50WPP4bMItIrfRKW3OD6B687cc-1718865139-1.0.1.1-.mwvto43wAJymXKhrq_Lqlp_ba8VTSGnK.gJIybKhx58G2v1OAzxK8vnaPL3TYpi3ZxPqixDdvlcdKwsM3Wb3g; - path=/; expires=Thu, 20-Jun-24 07:02:19 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=a_.VeTQxZ.cK71ENsv8KAzr3pvTjC2erYm8OfHUc6k0-1729142080-1.0.1.1-Vc6FrXuqqCq0WOqg0ymLI7NGWox9fXTDEwYhqprh.kCtVABiKu_g8Hd17F0YwnfVPV.WhfriqUIzyafBRYHgxw; + path=/; expires=Thu, 17-Oct-24 05:44:40 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=Udvyk9jWojWeH8XS41I1sDtjlX5USgzGucGhUFXBoZY-1718865139975-0.0.1.1-604800000; + - _cfuvid=s.6kDVD7lcPFo0no5zupQjvD8qA5_I_0Ey1A.NoE8Rs-1729142080313-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Transfer-Encoding: - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID alt-svc: - h3=":443"; ma=86400 openai-organization: - user-54labie7aicgek5urzpgydpm openai-processing-ms: - - '238' + - '631' openai-version: - '2020-10-01' strict-transport-security: - - max-age=31536000; includeSubDomains + - max-age=31536000; includeSubDomains; preload x-ratelimit-limit-requests: - '5000' x-ratelimit-limit-tokens: - - '450000' + - '800000' x-ratelimit-remaining-requests: - '4999' x-ratelimit-remaining-tokens: - - '449709' + - '799710' x-ratelimit-reset-requests: - 12ms x-ratelimit-reset-tokens: - - 38ms + - 21ms x-request-id: - - 92c7b6115df3895e7df67f76d1b624d1 + - req_9a70285ff1eaefa306a1dc36668e5055 status: code: 200 message: OK diff --git a/frontend/package.json b/frontend/package.json index 9e3b6dee..e5ff3f15 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,32 +14,36 @@ "@types/lodash": "^4.14.178", "@types/node": "^16.11.15", "@types/pdfjs-dist": "^2.10.378", - "@types/react": "^17.0.37", + "@types/react": "18", "@types/react-color": "^3.0.6", - "@types/react-dom": "^17.0.11", + "@types/react-dom": "18", "@types/react-pdf": "^5.0.9", "@types/react-syntax-highlighter": "^15.5.13", "@types/react-textfit": "^1.1.0", - "@types/styled-components": "^5.1.19", + "@uiw/react-color": "^2.3.2", + "@uiw/react-json-view": "^2.0.0-alpha.30", "axios": "^0.24.0", + "d3": "^7.9.0", "framer-motion": "6.*", "fuse.js": "^6.5.3", "graphql": "^16.2.0", + "history": "^5.3.0", "lodash": "^4.17.21", "lodash.uniqueid": "^4.0.1", "lucide-react": "^0.438.0", "pdfjs-dist": "^2.13.216", "polished": "^4.3.1", - "react": "17.*", + "react": "18", "react-beautiful-dnd": "^13.1.0", - "react-color": "^2.19.3", "react-cool-inview": "^3.0.1", "react-countup": "^6.5.3", - "react-dom": "^17.0.2", - "react-dropzone": "^11.4.2", + "react-data-grid": "^7.0.0-beta.47", + "react-dom": "18", + "react-dropzone": "^14.2.10", "react-infinite-scroll-component": "^6.1.0", "react-intersection-observer": "^9.1.0", "react-json-tree": "^0.19.0", + "react-json-view": "^1.21.3", "react-markdown": "8.0.7", "react-pdf": "^5.7.1", "react-router-dom": "6", @@ -48,9 +52,11 @@ "react-textfit": "^1.1.1", "react-toastify": "^8.1.0", "semantic-ui-css": "^2.4.1", - "semantic-ui-react": "^2.0.4", - "styled-components": "^5.3.3", + "semantic-ui-react": "2.1.5", + "styled-components": "^6.0.0", + "stylis": "^4.0.0", "typescript": "^4.5.4", + "use-debounce": "^10.0.3", "uuid": "^8.3.2", "web-vitals": "^2.1.2", "worker-loader": "^3.0.8" @@ -87,6 +93,7 @@ }, "devDependencies": { "@graphql-codegen/cli": "^5.0.2", + "@types/d3": "^7.4.3", "@types/lodash.uniqueid": "^4.0.9", "@types/uuid": "^8.3.4", "babel-jest": "^29.7.0", diff --git a/frontend/src/@types/react-json-view.d.ts b/frontend/src/@types/react-json-view.d.ts new file mode 100644 index 00000000..cddee744 --- /dev/null +++ b/frontend/src/@types/react-json-view.d.ts @@ -0,0 +1,29 @@ +declare module "@uiw/react-json-view" { + import * as React from "react"; + + interface EditAction { + updated_src: any; + existing_src: any; + name: string; + namespace: string; + src: any; + } + + interface JsonViewProps { + src: any; + theme?: object; + displayDataTypes?: boolean; + displayObjectSize?: boolean; + enableClipboard?: boolean; + onEdit?: (edit: EditAction) => void; + onAdd?: (add: EditAction) => void; + onDelete?: (del: EditAction) => void; + indentWidth?: number; + collapsed?: boolean | number; + [key: string]: any; // For any additional props + } + + const JsonView: React.FC; + + export default JsonView; +} diff --git a/frontend/src/App.css b/frontend/src/App.css index 8515b17d..83e19181 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -62,3 +62,30 @@ div > .SettingsPopup { #ConfirmModal { z-index: 20001 !important; } + +.ui.dimmer.modals.page:has(.high-z-index-modal) { + z-index: 99996 !important; +} + +.custom-data-grid { + border: none !important; + height: calc(100% - 50px) !important; +} + +.rdg-cell { + border-right: 1px solid #f0f0f0 !important; + border-bottom: 1px solid #f0f0f0 !important; + padding: 8px 16px !important; +} + +.rdg-header-row { + background-color: #f8f9fa !important; +} + +.rdg-row:hover { + background-color: #f8f9fa !important; +} + +.rdg-row.rdg-row-selected { + background-color: #e8f4ff !important; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cc73d6e9..f39dc9e4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; import { useAuth0 } from "@auth0/auth0-react"; @@ -23,11 +23,12 @@ import { selectedAnalyses, onlyDisplayTheseAnnotations, openedCorpus, - displayAnnotationOnAnnotatorLoad, showSelectedAnnotationOnly, showAnnotationBoundingBoxes, openedExtract, showSelectCorpusAnalyzerOrFieldsetModal, + showUploadNewDocumentsModal, + uploadModalPreloadedFiles, } from "./graphql/cache"; import { NavMenu } from "./components/layout/NavMenu"; @@ -49,13 +50,15 @@ import "./App.css"; import "react-toastify/dist/ReactToastify.css"; import useWindowDimensions from "./components/hooks/WindowDimensionHook"; import { MobileNavMenu } from "./components/layout/MobileNavMenu"; -import { LabelDisplayBehavior } from "./graphql/types"; +import { LabelDisplayBehavior } from "./types/graphql-api"; import { CookieConsentDialog } from "./components/cookies/CookieConsent"; import { Extracts } from "./views/Extracts"; import { useEnv } from "./components/hooks/UseEnv"; import { EditExtractModal } from "./components/widgets/modals/EditExtractModal"; import { SelectAnalyzerOrFieldsetModal } from "./components/widgets/modals/SelectCorpusAnalyzerOrFieldsetAnalyzer"; import { DocumentAnnotator } from "./components/annotator/DocumentAnnotator"; +import { DocumentUploadModal } from "./components/widgets/modals/DocumentUploadModal"; +import { FileUploadPackageProps } from "./components/widgets/modals/DocumentUploadModal"; export const App = () => { const { REACT_APP_USE_AUTH0 } = useEnv(); @@ -78,6 +81,9 @@ export const App = () => { showSelectCorpusAnalyzerOrFieldsetModal ); const show_annotation_labels = useReactiveVar(showAnnotationLabels); + const show_upload_new_documents_modal = useReactiveVar( + showUploadNewDocumentsModal + ); const { getAccessTokenSilently, user } = useAuth0(); @@ -125,6 +131,20 @@ export const App = () => { console.log("Cookie Accepted: ", show_cookie_modal); + const onDrop = useCallback((acceptedFiles: File[]) => { + const filePackages: FileUploadPackageProps[] = acceptedFiles.map( + (file) => ({ + file, + formData: { + title: file.name, + description: `Content summary for ${file.name}`, + }, + }) + ); + showUploadNewDocumentsModal(true); + uploadModalPreloadedFiles(filePackages); + }, []); + return (
{ ) : ( <> )} - + { + showUploadNewDocumentsModal(false); + uploadModalPreloadedFiles([]); + }} + open={Boolean(show_upload_new_documents_modal)} + onClose={() => { + showUploadNewDocumentsModal(false); + uploadModalPreloadedFiles([]); + }} + corpusId={opened_corpus?.id || null} + /> } /> {!REACT_APP_USE_AUTH0 ? ( diff --git a/frontend/src/assets/configurations/constants.ts b/frontend/src/assets/configurations/constants.ts index a18efe89..cbf1a34b 100644 --- a/frontend/src/assets/configurations/constants.ts +++ b/frontend/src/assets/configurations/constants.ts @@ -1,2 +1,2 @@ -export const VERSION_TAG = "v2.3.1"; +export const VERSION_TAG = "v2.4.0"; export const MOBILE_VIEW_BREAKPOINT = 600; diff --git a/frontend/src/components/analyses/AnalysesCards.tsx b/frontend/src/components/analyses/AnalysesCards.tsx index 74f78051..fc02b13a 100644 --- a/frontend/src/components/analyses/AnalysesCards.tsx +++ b/frontend/src/components/analyses/AnalysesCards.tsx @@ -5,7 +5,7 @@ import _ from "lodash"; import { AnalysisItem } from "./AnalysisItem"; import { PlaceholderCard } from "../placeholders/PlaceholderCard"; import { FetchMoreOnVisible } from "../widgets/infinite_scroll/FetchMoreOnVisible"; -import { AnalysisType, CorpusType, PageInfo } from "../../graphql/types"; +import { AnalysisType, CorpusType, PageInfo } from "../../types/graphql-api"; import { useReactiveVar } from "@apollo/client"; import { selectedAnalyses, selectedAnalysesIds } from "../../graphql/cache"; import useWindowDimensions from "../hooks/WindowDimensionHook"; diff --git a/frontend/src/components/analyses/AnalysisItem.tsx b/frontend/src/components/analyses/AnalysisItem.tsx index 12b0cbec..4b2ac539 100644 --- a/frontend/src/components/analyses/AnalysisItem.tsx +++ b/frontend/src/components/analyses/AnalysisItem.tsx @@ -18,7 +18,7 @@ import { REQUEST_DELETE_ANALYSIS, } from "../../graphql/mutations"; import { GetAnalysesOutputs, GET_ANALYSES } from "../../graphql/queries"; -import { AnalysisType, CorpusType } from "../../graphql/types"; +import { AnalysisType, CorpusType } from "../../types/graphql-api"; import _ from "lodash"; import { PermissionTypes } from "../types"; import { getPermissions } from "../../utils/transform"; @@ -242,7 +242,7 @@ export const AnalysisItem = ({ {analysis.analyzer.description} {isOverflowing && !showFullDescription && ( { + onClick={(e: React.MouseEvent) => { e.stopPropagation(); setShowFullDescription(true); }} diff --git a/frontend/src/components/analyses/AnalysisSelectorForCorpus.tsx b/frontend/src/components/analyses/AnalysisSelectorForCorpus.tsx index 11cd08bf..5644d652 100644 --- a/frontend/src/components/analyses/AnalysisSelectorForCorpus.tsx +++ b/frontend/src/components/analyses/AnalysisSelectorForCorpus.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo } from "react"; import { Segment, Form, Button, Popup, Icon } from "semantic-ui-react"; import Fuse from "fuse.js"; -import { AnalysisType, CorpusType, ExtractType } from "../../graphql/types"; +import { AnalysisType, CorpusType, ExtractType } from "../../types/graphql-api"; import { AnalysisItem } from "./AnalysisItem"; import { PlaceholderCard } from "../placeholders/PlaceholderCard"; import useWindowDimensions from "../hooks/WindowDimensionHook"; diff --git a/frontend/src/components/analyses/CorpusAnalysesCards.tsx b/frontend/src/components/analyses/CorpusAnalysesCards.tsx index 64bc941e..b425e621 100644 --- a/frontend/src/components/analyses/CorpusAnalysesCards.tsx +++ b/frontend/src/components/analyses/CorpusAnalysesCards.tsx @@ -67,6 +67,10 @@ export const CorpusAnalysesCards = () => { // Effects to reload data on certain changes /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + useEffect(() => { + refetchAnalyses(); + }, []); + useEffect(() => { refetchAnalyses(); }, [analysis_search_term]); diff --git a/frontend/src/components/analyzers/AnalyzerSummaryCard.tsx b/frontend/src/components/analyzers/AnalyzerSummaryCard.tsx index 6d2d87ec..76b25365 100644 --- a/frontend/src/components/analyzers/AnalyzerSummaryCard.tsx +++ b/frontend/src/components/analyzers/AnalyzerSummaryCard.tsx @@ -1,6 +1,6 @@ import { Card, Image, Button, List, Header, Dimmer } from "semantic-ui-react"; import analyzer_icon from "../../assets/icons/noun-epicyclic-gearing-800132.png"; -import { AnalyzerType, CorpusType } from "../../graphql/types"; +import { AnalyzerType, CorpusType } from "../../types/graphql-api"; export interface AnalyzerSummaryCardInputs { analyzer: AnalyzerType; diff --git a/frontend/src/components/annotations/AnnotationCards.tsx b/frontend/src/components/annotations/AnnotationCards.tsx index 15b4b9fe..d59f5321 100644 --- a/frontend/src/components/annotations/AnnotationCards.tsx +++ b/frontend/src/components/annotations/AnnotationCards.tsx @@ -27,7 +27,7 @@ import { PageInfo, CorpusType, DocumentType, -} from "../../graphql/types"; +} from "../../types/graphql-api"; import { FetchMoreOnVisible } from "../widgets/infinite_scroll/FetchMoreOnVisible"; import useWindowDimensions from "../hooks/WindowDimensionHook"; import { determineCardColCount } from "../../utils/layout"; @@ -224,6 +224,12 @@ export const AnnotationCards: React.FC = ({ label: "Extract", color: "#2196F3", }; + } else if (!item.analysis) { + return { + icon: , + label: "Manually-Annotated", + color: "#4CAF50", + }; } else { return { icon: , diff --git a/frontend/src/components/annotations/CorpusAnnotationCards.tsx b/frontend/src/components/annotations/CorpusAnnotationCards.tsx index 5f396340..c0ab06d6 100644 --- a/frontend/src/components/annotations/CorpusAnnotationCards.tsx +++ b/frontend/src/components/annotations/CorpusAnnotationCards.tsx @@ -14,6 +14,7 @@ import { filterToLabelId, selectedAnalyses, showCorpusActionOutputs, + filterToAnnotationType, } from "../../graphql/cache"; import { @@ -21,7 +22,7 @@ import { GetAnnotationsOutputs, GET_ANNOTATIONS, } from "../../graphql/queries"; -import { ServerAnnotationType } from "../../graphql/types"; +import { ServerAnnotationType } from "../../types/graphql-api"; export const CorpusAnnotationCards = ({ opened_corpus_id, @@ -40,7 +41,7 @@ export const CorpusAnnotationCards = ({ const filter_to_label_id = useReactiveVar(filterToLabelId); const selected_analyses = useReactiveVar(selectedAnalyses); const show_action_annotations = useReactiveVar(showCorpusActionOutputs); - + const filter_to_annotation_type = useReactiveVar(filterToAnnotationType); const location = useLocation(); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -58,12 +59,15 @@ export const CorpusAnnotationCards = ({ data: annotation_response, fetchMore: fetchMoreAnnotations, } = useQuery(GET_ANNOTATIONS, { + fetchPolicy: "network-only", notifyOnNetworkStatusChange: true, // necessary in order to trigger loading signal on fetchMore variables: { - annotationLabel_Type: "TOKEN_LABEL", createdByAnalysisIds: selected_analysis_id_string, analysis_Isnull: !show_action_annotations, ...(opened_corpus_id ? { corpusId: opened_corpus_id } : {}), + ...(filter_to_annotation_type + ? { annotationLabel_Type: filter_to_annotation_type } + : {}), ...(filter_to_label_id ? { annotationLabelId: filter_to_label_id } : {}), ...(filter_to_labelset_id ? { usesLabelFromLabelsetId: filter_to_labelset_id } @@ -156,8 +160,11 @@ export const CorpusAnnotationCards = ({ items={annotation_items} loading={annotation_loading} loading_message="Annotations Loading..." - pageInfo={undefined} - //pageInfo={annotation_response?.annotations?.pageInfo ? annotation_response.annotations.pageInfo : undefined} + pageInfo={ + annotation_response?.annotations?.pageInfo + ? annotation_response.annotations.pageInfo + : undefined + } style={{ minHeight: "70vh", overflowY: "unset" }} fetchMore={handleFetchMoreAnnotations} /> diff --git a/frontend/src/components/annotator/AnnotationSummary.tsx b/frontend/src/components/annotator/AnnotationSummary.tsx index f977ca01..a01932bd 100644 --- a/frontend/src/components/annotator/AnnotationSummary.tsx +++ b/frontend/src/components/annotator/AnnotationSummary.tsx @@ -3,7 +3,7 @@ import { RenderedSpanAnnotation, PDFStore, AnnotationStore, - ServerAnnotation, + ServerTokenAnnotation, } from "./context"; import { Label, Card } from "semantic-ui-react"; import styled from "styled-components"; @@ -21,7 +21,7 @@ export const AnnotationSummary = ({ annotation }: AnnotationSummaryProps) => { const annotationStore = useContext(AnnotationStore); const this_annotation = _.find(annotationStore.pdfAnnotations.annotations, { id: annotation, - }) as ServerAnnotation; + }) as ServerTokenAnnotation; if (!pdfStore.pages) { return null; diff --git a/frontend/src/components/annotator/DocumentAnnotator.tsx b/frontend/src/components/annotator/DocumentAnnotator.tsx index 08643c48..c0230a6c 100644 --- a/frontend/src/components/annotator/DocumentAnnotator.tsx +++ b/frontend/src/components/annotator/DocumentAnnotator.tsx @@ -1,11 +1,17 @@ -import { useLazyQuery, useQuery, useReactiveVar } from "@apollo/client"; +import { + useLazyQuery, + useQuery, + useReactiveVar, + QueryResult, +} from "@apollo/client"; import { useEffect, useState } from "react"; import { DocTypeAnnotation, PDFPageInfo, RelationGroup, - ServerAnnotation, + ServerSpanAnnotation, + ServerTokenAnnotation, } from "./context"; import { GET_DOCUMENT_ANALYSES_AND_EXTRACTS, @@ -23,7 +29,7 @@ import { } from "../../graphql/queries"; import { PDFDocumentProxy } from "pdfjs-dist/types/src/display/api"; -import { getPawlsLayer } from "./api/rest"; +import { getDocumentRawText, getPawlsLayer } from "./api/rest"; import { AnalysisRowType, AnalysisType, @@ -36,7 +42,7 @@ import { LabelDisplayBehavior, LabelType, ServerAnnotationType, -} from "../../graphql/types"; +} from "../../types/graphql-api"; import { ViewState, TokenId, @@ -73,7 +79,7 @@ import { Result } from "../widgets/data-display/Result"; import { SidebarContainer } from "../common"; import { CenterOnPage } from "./CenterOnPage"; import useWindowDimensions from "../hooks/WindowDimensionHook"; -import { AnnotatorRenderer } from "./display/AnnotatorRenderer"; +import { AnnotatorRenderer } from "./display/components/AnnotatorRenderer"; import { PDFDocumentLoadingTask } from "pdfjs-dist"; import { toast } from "react-toastify"; import { createTokenStringSearch } from "./utils"; @@ -144,12 +150,14 @@ export const DocumentAnnotator = ({ ); const [doc, setDocument] = useState(); + const [documentType, setDocumentType] = useState(""); // Hook 16 const [pages, setPages] = useState([]); // Hook 17 const [pageTextMaps, setPageTextMaps] = useState>(); + const [rawText, setRawText] = useState(""); // New states for analyses and extracts const [analyses, setAnalyses] = useState([]); @@ -159,9 +167,11 @@ export const DocumentAnnotator = ({ // Hook 22 const [structuralAnnotations, setStructuralAnnotations] = useState< - ServerAnnotation[] + ServerTokenAnnotation[] + >([]); + const [annotation_objs, setAnnotationObjs] = useState< + (ServerTokenAnnotation | ServerSpanAnnotation)[] >([]); - const [annotation_objs, setAnnotationObjs] = useState([]); const [doc_type_annotations, setDocTypeAnnotations] = useState< DocTypeAnnotation[] >([]); @@ -230,6 +240,11 @@ export const DocumentAnnotator = ({ allowUserInput(false); }, [editMode]); + // store doc type in state + useEffect(() => { + setDocumentType(opened_document.fileType ? opened_document.fileType : ""); + }, [opened_document]); + // Hook #37 let corpus_id = opened_corpus?.id; let analysis_vars = { @@ -246,6 +261,7 @@ export const DocumentAnnotator = ({ >(GET_DOCUMENT_ANALYSES_AND_EXTRACTS, { variables: analysis_vars, skip: Boolean(displayOnlyTheseAnnotations), + fetchPolicy: "network-only", }); // Hook #38 @@ -289,7 +305,7 @@ export const DocumentAnnotator = ({ // Calculated control and display variables - in certain situations we want to change behavior based on available data or // selected configurations. - // Depending on the edit mode and some state variables, we may wany to load all annotations for the document + // Depending on the edit mode and some state variables, we may want to load all annotations for the document // Particularly of node is when displayOnlyTheseAnnotations is set to something, we don't want to load additional // annotations. We are just rendering and displaying the annotations stored in this state variable. // TODO - load annotations on a page-by-page basis to cut down on server load. @@ -304,6 +320,9 @@ export const DocumentAnnotator = ({ variables: { documentId: opened_document.id, corpusId: opened_corpus.id, + ...(selected_analysis + ? { analysisId: selected_analysis.id } + : { analysisId: "__none__" }), }, }); } @@ -317,6 +336,28 @@ export const DocumentAnnotator = ({ (annotation) => convertToServerAnnotation(annotation) ) ?? []; + setAnnotationObjs((prevAnnotations) => { + const updatedAnnotations = prevAnnotations.map((prevAnnot) => { + const matchingNewAnnot = processedAnnotations.find( + (newAnnot) => newAnnot.id === prevAnnot.id + ); + return matchingNewAnnot + ? { ...prevAnnot, ...matchingNewAnnot } + : prevAnnot; + }); + + const newAnnotations = processedAnnotations.filter( + (newAnnot) => + !prevAnnotations.some((prevAnnot) => prevAnnot.id === newAnnot.id) + ); + + return [...updatedAnnotations, ...newAnnotations] as ( + | ServerTokenAnnotation + | ServerSpanAnnotation + )[]; + }); + + // Process relationships similarly if needed const processedRelationships = humanAnnotationsAndRelationshipsData.document?.allRelationships?.map( (relationship) => @@ -332,11 +373,21 @@ export const DocumentAnnotator = ({ ) ) ?? []; - setAnnotationObjs((prevAnnotations) => [ - ...prevAnnotations, - ...processedAnnotations, - ]); - setRelationshipAnnotations(processedRelationships); + setRelationshipAnnotations((prevRelationships) => { + const updatedRelationships = prevRelationships.map((prevRel) => { + const matchingNewRel = processedRelationships.find( + (newRel) => newRel.id === prevRel.id + ); + return matchingNewRel || prevRel; + }); + + const newRelationships = processedRelationships.filter( + (newRel) => + !prevRelationships.some((prevRel) => prevRel.id === newRel.id) + ); + + return [...updatedRelationships, ...newRelationships]; + }); // Use labelSet.allAnnotationLabels to set the labels const allLabels = @@ -344,21 +395,27 @@ export const DocumentAnnotator = ({ ?.allAnnotationLabels ?? []; // Filter and set span labels - const spanLabels = allLabels.filter( - (label) => label.labelType === "TOKEN_LABEL" + // Filter labels based on document type + const relevantLabelType = + documentType === "application/pdf" + ? LabelType.TokenLabel + : LabelType.SpanLabel; + const relevantLabels = allLabels.filter( + (label) => label.labelType === relevantLabelType ); - setSpanLabels(spanLabels); - setHumanSpanLabels(spanLabels); + + setSpanLabels(relevantLabels); + setHumanSpanLabels(relevantLabels); // Filter and set relation labels const relationLabels = allLabels.filter( - (label) => label.labelType === "RELATIONSHIP_LABEL" + (label) => label.labelType === LabelType.RelationshipLabel ); setRelationLabels(relationLabels); // Filter and set document labels (if needed) const docLabels = allLabels.filter( - (label) => label.labelType === "DOC_TYPE_LABEL" + (label) => label.labelType === LabelType.DocTypeLabel ); setDocTypeLabels(docLabels); } @@ -415,92 +472,182 @@ export const DocumentAnnotator = ({ // Effect to load document and pawls layer useEffect(() => { - if (open && opened_document && opened_document.pdfFile) { + if (open && opened_document) { + console.log( + "React to DocumentAnnotator opening or document change", + opened_document + ); + viewStateVar(ViewState.LOADING); fetchDocumentAnalysesAndExtracts(); - const loadingTask: PDFDocumentLoadingTask = pdfjsLib.getDocument( - opened_document.pdfFile - ); - loadingTask.onProgress = (p: { loaded: number; total: number }) => { - setProgress(Math.round((p.loaded / p.total) * 100)); + + const loadAnnotations = () => { + if (opened_corpus?.labelSet && !displayOnlyTheseAnnotations) { + return getDocumentAnnotationsAndRelationships({ + variables: { + documentId: opened_document.id, + corpusId: opened_corpus.id, + ...(selected_analysis + ? { analysisId: selected_analysis.id } + : { analysisId: "__none__" }), + }, + }); + } + return Promise.resolve(null); }; - // If we're in annotate mode and corpus has a labelset AND we don't have initial annotations to display if ( - edit_mode === "ANNOTATE" && - opened_corpus?.labelSet && - (!displayOnlyTheseAnnotations || - displayOnlyTheseAnnotations.length == 0) + opened_document.fileType === "application/pdf" && + opened_document.pdfFile ) { - getDocumentAnnotationsAndRelationships({ - variables: { - documentId: opened_document.id, - corpusId: opened_corpus.id, - }, - }); + const loadingTask: PDFDocumentLoadingTask = pdfjsLib.getDocument( + opened_document.pdfFile + ); + loadingTask.onProgress = (p: { loaded: number; total: number }) => { + setProgress(Math.round((p.loaded / p.total) * 100)); + }; + + Promise.all([ + loadingTask.promise, + getPawlsLayer(opened_document.pawlsParseFile || ""), + loadAnnotations(), + ]) + .then( + ([pdfDoc, pawlsData, annotationsData]: [ + PDFDocumentProxy, + PageTokens[], + QueryResult< + GetDocumentAnnotationsAndRelationshipsOutput, + GetDocumentAnnotationsAndRelationshipsInput + > | null + ]) => { + console.log("Retrieved annotations data:", annotationsData); + + setDocument(pdfDoc); + processAnnotationsData(annotationsData); + + const loadPages: Promise[] = []; + for (let i = 1; i <= pdfDoc.numPages; i++) { + loadPages.push( + pdfDoc.getPage(i).then((p) => { + let pageTokens: Token[] = []; + if (pawlsData.length === 0) { + toast.error( + "Token layer isn't available for this document... annotations can't be displayed." + ); + } else { + const pageIndex = p.pageNumber - 1; + pageTokens = pawlsData[pageIndex].tokens; + } + return new PDFPageInfo(p, pageTokens, zoom_level); + }) as unknown as Promise + ); + } + return Promise.all(loadPages); + } + ) + .then((pages) => { + setPages(pages); + let { doc_text, string_index_token_map } = + createTokenStringSearch(pages); + setPageTextMaps({ + ...string_index_token_map, + ...pageTextMaps, + }); + setRawText(doc_text); + // Loaded state set by useEffect for state change in doc state store. + }) + .catch((err) => { + console.error("Error loading PDF document:", err); + viewStateVar(ViewState.ERROR); + }); + } else if (opened_document.fileType === "application/txt") { + console.log("React to TXT document"); + + Promise.all([ + getDocumentRawText(opened_document.txtExtractFile || ""), + loadAnnotations(), + ]) + .then( + ([txt, annotationsData]: [ + string, + QueryResult< + GetDocumentAnnotationsAndRelationshipsOutput, + GetDocumentAnnotationsAndRelationshipsInput + > | null + ]) => { + console.log("Retrieved annotations data:", annotationsData); + + setRawText(txt); + processAnnotationsData(annotationsData); + viewStateVar(ViewState.LOADED); + } + ) + .catch((err) => { + console.error("Error loading TXT document:", err); + viewStateVar(ViewState.ERROR); + }); } + } + }, [open, opened_document, opened_corpus, displayOnlyTheseAnnotations]); + + const processAnnotationsData = ( + data: QueryResult< + GetDocumentAnnotationsAndRelationshipsOutput, + GetDocumentAnnotationsAndRelationshipsInput + > | null + ) => { + console.log("Processing annotations data:", data); + if (data?.data?.document) { + const processedAnnotations = + data.data.document.allAnnotations?.map((annotation) => + convertToServerAnnotation(annotation) + ) ?? []; + setAnnotationObjs(processedAnnotations); - // Load PDF and pawls layer - Promise.all([ - loadingTask.promise, - getPawlsLayer(opened_document.pawlsParseFile || ""), - opened_document.allStructuralAnnotations || Promise.resolve([]), - ]) - .then( - ([pdfDoc, pawlsData, structuralAnns]: [ - PDFDocumentProxy, - PageTokens[], - ServerAnnotationType[] - ]) => { - setDocument(pdfDoc); - - setStructuralAnnotations( - structuralAnns.map((annotation) => - convertToServerAnnotation(annotation) - ) - ); - - const loadPages: Promise[] = []; - for (let i = 1; i <= pdfDoc.numPages; i++) { - // See line 50 for an explanation of the cast here. - loadPages.push( - pdfDoc.getPage(i).then((p) => { - let pageTokens: Token[] = []; - if (pawlsData.length === 0) { - toast.error( - "Token layer isn't available for this document... annotations can't be displayed." - ); - // console.log("Loading up some data for page ", i, p); - } else { - const pageIndex = p.pageNumber - 1; - pageTokens = pawlsData[pageIndex].tokens; - } - - // console.log("Tokens", pageTokens); - return new PDFPageInfo(p, pageTokens, zoom_level); - }) as unknown as Promise - ); - } - return Promise.all(loadPages); - } - ) - .then((pages) => { - setPages(pages); + if (data.data.document?.allStructuralAnnotations) { + const structuralAnns = data.data.document.allStructuralAnnotations.map( + (ann) => convertToServerAnnotation(ann) + ); + setStructuralAnnotations(structuralAnns); + } - let { doc_text, string_index_token_map } = - createTokenStringSearch(pages); + const processedRelationships = data.data.document.allRelationships?.map( + (rel) => + new RelationGroup( + rel.sourceAnnotations.edges + .map((edge) => edge?.node?.id) + .filter((id): id is string => id !== undefined), + rel.targetAnnotations.edges + .map((edge) => edge?.node?.id) + .filter((id): id is string => id !== undefined), + rel.relationshipLabel, + rel.id + ) + ); + setRelationshipAnnotations(processedRelationships ?? []); - setPageTextMaps({ - ...string_index_token_map, - ...pageTextMaps, - }); - }) - .catch((err) => { - console.error("Error loading document:", err); - viewStateVar(ViewState.ERROR); - }); + if (data.data.corpus && data.data.corpus.labelSet) { + const allLabels = data.data.corpus.labelSet.allAnnotationLabels ?? []; + setSpanLabels( + allLabels.filter((label) => label.labelType === LabelType.SpanLabel) + ); + setHumanSpanLabels( + allLabels.filter((label) => label.labelType === LabelType.SpanLabel) + ); + setRelationLabels( + allLabels.filter( + (label) => label.labelType === LabelType.RelationshipLabel + ) + ); + setDocTypeLabels( + allLabels.filter( + (label) => label.labelType === LabelType.DocTypeLabel + ) + ); + } } - }, [open, opened_document]); + }; // If analysis or extract is deselected, try to refetch the data useEffect(() => { @@ -512,8 +659,11 @@ export const DocumentAnnotator = ({ // Only trigger state flip to "Loaded" if PDF, pageTextMaps and page info load properly useEffect(() => { - if (doc && pageTextMaps && pages.length > 0) { - viewStateVar(ViewState.LOADED); + if (opened_document.fileType === "application/pdf") { + if (doc && pageTextMaps && pages.length > 0) { + console.log("React to PDF document loading properly", doc); + viewStateVar(ViewState.LOADED); + } } }, [pageTextMaps, pages, doc]); @@ -533,6 +683,10 @@ export const DocumentAnnotator = ({ // If we got a property of annotations to display (and ONLY those), do some post processing and update state variable(s) accordingly useEffect(() => { + console.log( + "React to displayOnlyTheseAnnotations", + displayOnlyTheseAnnotations + ); if (displayOnlyTheseAnnotations) { setAnnotationObjs( convertToServerAnnotations(displayOnlyTheseAnnotations) @@ -578,7 +732,9 @@ export const DocumentAnnotator = ({ // TODO - properly parse resulting annotation data if (data && data.analysis && data.analysis.fullAnnotationList) { const rawSpanAnnotations = data.analysis.fullAnnotationList.filter( - (annot) => annot.annotationLabel.labelType == LabelType.TokenLabel + (annot) => + annot.annotationLabel.labelType === LabelType.TokenLabel || + annot.annotationLabel.labelType === LabelType.SpanLabel ); const rawDocAnnotations = data.analysis.fullAnnotationList.filter( (annot) => annot.annotationLabel.labelType == LabelType.DocTypeLabel @@ -673,6 +829,7 @@ export const DocumentAnnotator = ({ }; let rendered_component = <>; + console.log("view_state", view_state, ViewState.LOADING, ViewState.LOADED); switch (view_state) { case ViewState.LOADING: rendered_component = ( @@ -720,9 +877,9 @@ export const DocumentAnnotator = ({ selected_analysis={selected_analysis} selected_extract={selected_extract} allowInput={false} + editMode="ANNOTATE" datacells={data_cells} columns={columns} - editMode="ANNOTATE" setEditMode={(v: "ANALYZE" | "ANNOTATE") => {}} setAllowInput={(v: boolean) => {}} /> @@ -734,64 +891,68 @@ export const DocumentAnnotator = ({ ); break; case ViewState.LOADED: - if (doc) { - rendered_component = ( - { - editMode(m); - }} - allowInput={allow_input} - setAllowInput={(v: boolean) => { - allowUserInput(v); - }} - analyses={analyses} - extracts={extracts} - selected_analysis={selected_analysis} - selected_extract={selected_extract} - onSelectAnalysis={onSelectAnalysis} - onSelectExtract={onSelectExtract} - onError={(vs: ViewState) => { - viewStateVar(vs); - }} - /> - ); - } + rendered_component = ( + { + editMode(m); + }} + allowInput={allow_input} + setAllowInput={(v: boolean) => { + allowUserInput(v); + }} + analyses={analyses} + extracts={extracts} + selected_analysis={selected_analysis} + selected_extract={selected_extract} + onSelectAnalysis={onSelectAnalysis} + onSelectExtract={onSelectExtract} + onError={(vs: ViewState) => { + viewStateVar(vs); + }} + /> + ); break; // eslint-disable-line: no-fallthrough case ViewState.ERROR: @@ -826,7 +987,10 @@ export const DocumentAnnotator = ({ className="AnnotatorModal" closeIcon open={open} - onClose={onClose} + onClose={() => { + onClose(); + setDocument(undefined); + }} size="fullscreen" > { return axios.get(url).then((r) => r.data); } + +export async function getDocumentRawText(url: string): Promise { + return axios.get(url).then((content) => content.data); +} diff --git a/frontend/src/components/annotator/context/AnnotationStore.ts b/frontend/src/components/annotator/context/AnnotationStore.ts index 92a9e18c..f5ad02d0 100644 --- a/frontend/src/components/annotator/context/AnnotationStore.ts +++ b/frontend/src/components/annotator/context/AnnotationStore.ts @@ -1,14 +1,16 @@ import { createContext } from "react"; import { v4 as uuidv4 } from "uuid"; -import { AnnotationLabelType } from "../../../graphql/types"; +import { AnnotationLabelType, LabelType } from "../../../types/graphql-api"; import { PDFPageInfo } from "."; import { BoundingBox, MultipageAnnotationJson, PermissionTypes, + SpanAnnotationJson, + TextSearchSpanResult, } from "../../types"; -import { TextSearchResult } from "../../types"; +import { TextSearchTokenResult } from "../../types"; export interface TokenId { pageIndex: number; @@ -26,7 +28,9 @@ export class RelationGroup { } // TODO - need to find a way to integrate this into current application log, which does NOT account for this. - updateForAnnotationDeletion(a: ServerAnnotation): RelationGroup | undefined { + updateForAnnotationDeletion( + a: ServerTokenAnnotation | ServerSpanAnnotation + ): RelationGroup | undefined { const sourceEmpty = this.sourceIds.length === 0; const targetEmpty = this.targetIds.length === 0; @@ -63,7 +67,60 @@ export class RelationGroup { } } -export class ServerAnnotation { +export class ServerSpanAnnotation { + public readonly id: string; + + constructor( + public readonly page: number, + public readonly annotationLabel: AnnotationLabelType, + public readonly rawText: string, + public readonly structural: boolean, + public readonly json: SpanAnnotationJson, + public readonly myPermissions: PermissionTypes[], + public readonly approved: boolean, + public readonly rejected: boolean, + public readonly canComment: boolean = false, + id: string | undefined = undefined + ) { + this.id = id || uuidv4(); + } + + toString() { + return this.id; + } + + update(delta: Partial = {}): ServerSpanAnnotation { + return new ServerSpanAnnotation( + delta.page ?? this.page, + delta.annotationLabel ?? Object.assign({}, this.annotationLabel), + delta.rawText ?? this.rawText, + delta.structural ?? this.structural, + delta.json ?? this.json, + delta.myPermissions ?? this.myPermissions, + delta.approved ?? this.approved, + delta.rejected ?? this.rejected, + delta.canComment ?? this.canComment, + this.id + ); + } + + static fromObject(obj: ServerSpanAnnotation): ServerSpanAnnotation { + return new ServerSpanAnnotation( + obj.page, + obj.annotationLabel, + obj.rawText, + obj.structural, + obj.json, + obj.myPermissions, + obj.approved, + obj.rejected, + obj.canComment, + obj.id + ); + } +} + +export class ServerTokenAnnotation { public readonly id: string; constructor( @@ -89,8 +146,8 @@ export class ServerAnnotation { * Returns a deep copy of the provided Annotation with the applied * changes. */ - update(delta: Partial = {}) { - return new ServerAnnotation( + update(delta: Partial = {}) { + return new ServerTokenAnnotation( delta.page ?? this.page, delta.annotationLabel ?? Object.assign({}, this.annotationLabel), delta.rawText ?? this.rawText, @@ -104,8 +161,8 @@ export class ServerAnnotation { ); } - static fromObject(obj: ServerAnnotation) { - return new ServerAnnotation( + static fromObject(obj: ServerTokenAnnotation) { + return new ServerTokenAnnotation( obj.page, obj.annotationLabel, obj.rawText, @@ -191,7 +248,10 @@ export class DocTypeAnnotation { export class PdfAnnotations { constructor( - public readonly annotations: ServerAnnotation[], + public readonly annotations: ( + | ServerTokenAnnotation + | ServerSpanAnnotation + )[], public readonly relations: RelationGroup[], public readonly docTypes: DocTypeAnnotation[], public readonly unsavedChanges: boolean = false @@ -233,7 +293,7 @@ interface _AnnotationStore { activeSpanLabel?: AnnotationLabelType | undefined; hideSidebar: boolean; setHideSidebar: (hide: boolean) => void; - showOnlySpanLabels?: AnnotationLabelType[]; + showOnlySpanLabels?: AnnotationLabelType[] | null; setActiveLabel: (label: AnnotationLabelType) => void; scrollContainerRef: React.RefObject | undefined; @@ -274,7 +334,7 @@ interface _AnnotationStore { docTypeLabels: AnnotationLabelType[]; docText: string | undefined; - textSearchMatches: TextSearchResult[]; + textSearchMatches: (TextSearchTokenResult | TextSearchSpanResult)[]; selectedTextSearchMatchIndex: number; searchText: string | undefined; allowComment: boolean; @@ -297,9 +357,9 @@ interface _AnnotationStore { setPdfPageInfoObjs: (b: Record) => void; createMultiPageAnnotation: () => void; - createAnnotation: (a: ServerAnnotation) => void; + createAnnotation: (a: ServerTokenAnnotation | ServerSpanAnnotation) => void; deleteAnnotation: (annotation_id: string) => void; - updateAnnotation: (a: ServerAnnotation) => void; + updateAnnotation: (a: ServerTokenAnnotation | ServerSpanAnnotation) => void; clearViewLabels: () => void; setViewLabels: (ls: AnnotationLabelType[]) => void; @@ -426,13 +486,13 @@ export const AnnotationStore = createContext<_AnnotationStore>({ throw new Error("setActiveRelationLabel() - Unimplemented"); }, docTypeLabels: [], - createAnnotation: (_?: ServerAnnotation) => { + createAnnotation: (_?: ServerTokenAnnotation | ServerSpanAnnotation) => { throw new Error("createAnnotation() - Unimplemented"); }, deleteAnnotation: (_?: string) => { throw new Error("deleteAnnotation() - Unimplemented"); }, - updateAnnotation: (_?: ServerAnnotation) => { + updateAnnotation: (_?: ServerTokenAnnotation | ServerSpanAnnotation) => { throw new Error("updateAnnotation() - Unimplemented"); }, createDocTypeAnnotation: (_?: DocTypeAnnotation) => { diff --git a/frontend/src/components/annotator/context/PDFStore.ts b/frontend/src/components/annotator/context/PDFStore.ts index 85793675..8eb6e16a 100644 --- a/frontend/src/components/annotator/context/PDFStore.ts +++ b/frontend/src/components/annotator/context/PDFStore.ts @@ -5,7 +5,7 @@ import { } from "pdfjs-dist/types/src/display/api"; import { BoundingBox, SinglePageAnnotationJson, Token } from "../../types"; -import { AnnotationLabelType } from "../../../graphql/types"; +import { AnnotationLabelType } from "../../../types/graphql-api"; import { TokenId, RenderedSpanAnnotation } from "./AnnotationStore"; import { convertAnnotationTokensToText } from "../utils"; diff --git a/frontend/src/components/annotator/display/ActionBar.tsx b/frontend/src/components/annotator/display/components/ActionBar.tsx similarity index 85% rename from frontend/src/components/annotator/display/ActionBar.tsx rename to frontend/src/components/annotator/display/components/ActionBar.tsx index 69f868aa..b4e50451 100644 --- a/frontend/src/components/annotator/display/ActionBar.tsx +++ b/frontend/src/components/annotator/display/components/ActionBar.tsx @@ -1,10 +1,10 @@ -import React, { useContext, useRef, useState } from "react"; +import React, { useContext, useRef, useState, useCallback } from "react"; import { Form, Icon, Popup, Menu, SemanticICONS } from "semantic-ui-react"; import { Search, X } from "lucide-react"; import styled from "styled-components"; import _ from "lodash"; -import { AnnotationStore } from "../context"; // Adjust the import path as needed -import { ZoomButtonGroup } from "../../widgets/buttons/ZoomButtonGroup"; +import { AnnotationStore } from "../../context"; // Adjust the import path as needed +import { ZoomButtonGroup } from "../../../widgets/buttons/ZoomButtonGroup"; const ActionBar = styled.div` padding: 12px 16px; @@ -75,30 +75,23 @@ export const PDFActionBar: React.FC = ({ const annotationStore = useContext(AnnotationStore); const [isPopupOpen, setIsPopupOpen] = useState(false); - const { - textSearchMatches, - searchForText, - searchText, - selectedTextSearchMatchIndex, - } = annotationStore; + const { searchForText, searchText } = annotationStore; - const [docSearchCache, setDocSeachCache] = useState( - searchText + const debouncedDocSearch = useCallback( + _.debounce((searchTerm: string) => { + console.log("Searching for", searchTerm); + searchForText(searchTerm); + }, 300), + [searchForText] ); const handleDocSearchChange = (value: string) => { - setDocSeachCache(value); - debouncedDocSearch.current(value); + searchForText(value); + debouncedDocSearch(value); }; - const debouncedDocSearch = useRef( - _.debounce((searchTerm: string) => { - searchForText(searchTerm); - }, 300) - ); - const clearSearch = () => { - setDocSeachCache(""); + searchForText(""); searchForText(""); }; @@ -171,10 +164,10 @@ export const PDFActionBar: React.FC = ({ } iconPosition="left" placeholder="Search document..." + value={searchText} onChange={(e: any, data: { value: string }) => handleDocSearchChange(data.value) } - value={docSearchCache} /> diff --git a/frontend/src/components/annotator/display/AnnotatorRenderer.tsx b/frontend/src/components/annotator/display/components/AnnotatorRenderer.tsx similarity index 90% rename from frontend/src/components/annotator/display/AnnotatorRenderer.tsx rename to frontend/src/components/annotator/display/components/AnnotatorRenderer.tsx index 7a842779..8f0154ca 100644 --- a/frontend/src/components/annotator/display/AnnotatorRenderer.tsx +++ b/frontend/src/components/annotator/display/components/AnnotatorRenderer.tsx @@ -31,32 +31,39 @@ import { UpdateAnnotationOutputType, UpdateRelationInputType, UpdateRelationOutputType, -} from "../../../graphql/mutations"; -import { PDFView } from "../pages"; +} from "../../../../graphql/mutations"; +import { DocumentViewer } from "../viewer"; import { DocTypeAnnotation, PdfAnnotations, PDFPageInfo, RelationGroup, - ServerAnnotation, -} from "../context"; + ServerSpanAnnotation, + ServerTokenAnnotation, +} from "../../context"; import { PDFDocumentProxy } from "pdfjs-dist/types/src/display/api"; import { AnalysisType, AnnotationLabelType, + AnnotationTypeEnum, ColumnType, CorpusType, DatacellType, DocumentType, ExtractType, LabelDisplayBehavior, -} from "../../../graphql/types"; -import { ViewState, TokenId, PermissionTypes } from "../../types"; + LabelType, +} from "../../../../types/graphql-api"; +import { + ViewState, + TokenId, + PermissionTypes, + SpanAnnotationJson, +} from "../../../types"; import { toast } from "react-toastify"; -import { createTokenStringSearch } from "../utils"; -import { getPermissions } from "../../../utils/transform"; +import { getPermissions } from "../../../../utils/transform"; import _ from "lodash"; export interface TextSearchResultsProps { @@ -76,7 +83,9 @@ export interface PageTokenMapBuilderProps { interface AnnotatorRendererProps { open: boolean; - doc: PDFDocumentProxy; + doc: PDFDocumentProxy | undefined; + rawText: string; + pageTextMaps: Record | undefined; data_loading?: boolean; loading_message?: string; pages: PDFPageInfo[]; @@ -93,8 +102,8 @@ interface AnnotatorRendererProps { onSelectExtract?: (extract: ExtractType | null) => undefined | null | void; read_only: boolean; load_progress: number; - scrollToAnnotation?: ServerAnnotation; - selectedAnnotation?: ServerAnnotation[]; + scrollToAnnotation?: ServerTokenAnnotation | ServerSpanAnnotation; + selectedAnnotation?: (ServerTokenAnnotation | ServerSpanAnnotation)[]; show_selected_annotation_only: boolean; show_annotation_bounding_boxes: boolean; show_structural_annotations: boolean; @@ -103,12 +112,12 @@ interface AnnotatorRendererProps { human_span_labels: AnnotationLabelType[]; relationship_labels: AnnotationLabelType[]; document_labels: AnnotationLabelType[]; - annotation_objs: ServerAnnotation[]; + annotation_objs: (ServerTokenAnnotation | ServerSpanAnnotation)[]; doc_type_annotations: DocTypeAnnotation[]; relationship_annotations: RelationGroup[]; data_cells?: DatacellType[]; columns?: ColumnType[]; - structural_annotations?: ServerAnnotation[]; + structural_annotations?: ServerTokenAnnotation[]; editMode: "ANNOTATE" | "ANALYZE"; allowInput: boolean; setEditMode: (m: "ANNOTATE" | "ANALYZE") => void | undefined | null; @@ -120,6 +129,8 @@ interface AnnotatorRendererProps { export const AnnotatorRenderer = ({ doc, + rawText, + pageTextMaps, pages, data_loading, loading_message, @@ -164,9 +175,6 @@ export const AnnotatorRenderer = ({ new PdfAnnotations([], [], []) ); - const [pageTextMaps, setPageTextMaps] = useState>(); - const [doc_text, setDocText] = useState(""); - // New state to track if we've scrolled to the annotation const [hasScrolledToAnnotation, setHasScrolledToAnnotation] = useState< string | null @@ -204,7 +212,7 @@ export const AnnotatorRenderer = ({ // Refs for search results const textSearchElementRefs = useRef>({}); - const handleKeyUpPress = useCallback((event) => { + const handleKeyUpPress = useCallback((event: { keyCode: any }) => { const { keyCode } = event; if (keyCode === 16) { //console.log("Shift released"); @@ -212,7 +220,7 @@ export const AnnotatorRenderer = ({ } }, []); - const handleKeyDownPress = useCallback((event) => { + const handleKeyDownPress = useCallback((event: { keyCode: any }) => { const { keyCode } = event; if (keyCode === 16) { //console.log("Shift depressed") @@ -270,18 +278,7 @@ export const AnnotatorRenderer = ({ setHasScrolledToAnnotation(null); }, [scrollToAnnotation]); - // When the opened document is changed... reload... - useEffect(() => { - let { doc_text, string_index_token_map } = createTokenStringSearch(pages); - - setPageTextMaps({ - ...string_index_token_map, - ...pageTextMaps, - }); - setDocText(doc_text); - }, [pages, doc]); - - function addMultipleAnnotations(a: ServerAnnotation[]): void { + function addMultipleAnnotations(a: ServerTokenAnnotation[]): void { setPdfAnnotations( new PdfAnnotations( pdfAnnotations.annotations.concat(a), @@ -298,7 +295,7 @@ export const AnnotatorRenderer = ({ >(REQUEST_ADD_ANNOTATION); const requestCreateAnnotation = ( - added_annotation_obj: ServerAnnotation + added_annotation_obj: ServerTokenAnnotation | ServerSpanAnnotation ): void => { if (openedCorpus) { // Stray clicks on the canvas can trigger the annotation submission with empty token arrays and @@ -317,32 +314,60 @@ export const AnnotatorRenderer = ({ annotationLabelId: added_annotation_obj.annotationLabel.id, rawText: added_annotation_obj.rawText, page: added_annotation_obj.page, + annotationType: + added_annotation_obj instanceof ServerSpanAnnotation + ? LabelType.SpanLabel + : LabelType.TokenLabel, }, }) .then((data) => { toast.success("Annotated!\nAdded your annotation to the database."); //console.log("New annoation,", data); - let newRenderedAnnotations: ServerAnnotation[] = []; + let newRenderedAnnotations: ( + | ServerTokenAnnotation + | ServerSpanAnnotation + )[] = []; let annotationObj = data?.data?.addAnnotation?.annotation; if (annotationObj) { - newRenderedAnnotations.push( - new ServerAnnotation( - annotationObj.page, - annotationObj.annotationLabel, - annotationObj.rawText, - false, - annotationObj.json, - getPermissions( - annotationObj?.myPermissions - ? annotationObj.myPermissions - : [] - ), - false, - false, - false, - annotationObj.id - ) - ); + if (openedDocument.fileType === "application/txt") { + newRenderedAnnotations.push( + new ServerSpanAnnotation( + annotationObj.page, + annotationObj.annotationLabel, + annotationObj.rawText, + false, + annotationObj.json as SpanAnnotationJson, + getPermissions( + annotationObj?.myPermissions + ? annotationObj.myPermissions + : [] + ), + false, + false, + false, + annotationObj.id + ) + ); + } else { + newRenderedAnnotations.push( + new ServerTokenAnnotation( + annotationObj.page, + annotationObj.annotationLabel, + annotationObj.rawText, + false, + annotationObj.json, + getPermissions( + annotationObj?.myPermissions + ? annotationObj.myPermissions + : [] + ), + false, + false, + false, + annotationObj.id + ) + ); + } } addMultipleAnnotations(newRenderedAnnotations); }) @@ -464,11 +489,13 @@ export const AnnotatorRenderer = ({ }; } - function removeAnnotation(id: string): ServerAnnotation[] { + function removeAnnotation(id: string): ServerTokenAnnotation[] { return pdfAnnotations.annotations.filter((ann) => ann.id !== id); } - const requestUpdateAnnotation = (updated_annotation: ServerAnnotation) => { + const requestUpdateAnnotation = ( + updated_annotation: ServerTokenAnnotation + ) => { updateAnnotation({ variables: { id: updated_annotation.id, @@ -918,9 +945,9 @@ export const AnnotatorRenderer = ({ }; const replaceAnnotations = ( - replacement_annotations: ServerAnnotation[], - obj_list_to_replace_in: ServerAnnotation[] - ): ServerAnnotation[] => { + replacement_annotations: ServerTokenAnnotation[], + obj_list_to_replace_in: ServerTokenAnnotation[] + ): ServerTokenAnnotation[] => { const updated_ids = replacement_annotations.map((a) => a.id); const unchanged_annotations = obj_list_to_replace_in.filter( (a) => !updated_ids.includes(a.id) @@ -984,8 +1011,9 @@ export const AnnotatorRenderer = ({ } }; + console.log("AnnotatorRenderer..."); return ( - ` + position: absolute; + background-color: ${(props) => props.color || "yellow"}; + opacity: ${(props) => (props.highOpacity ? 0.5 : 0.3)}; + pointer-events: none; + ${(props) => + props.isSelected && + ` + border: 2px solid blue; + `} + ${(props) => props.left !== undefined && `left: ${props.left}px;`} + ${(props) => props.right !== undefined && `right: ${props.right}px;`} + ${(props) => props.top !== undefined && `top: ${props.top}px;`} + ${(props) => props.bottom !== undefined && `bottom: ${props.bottom}px;`} +`; export interface SelectionTokenGroupProps { id?: string; @@ -44,7 +73,7 @@ export const SelectionTokenGroup = ({ pageInfo.tokens[t.tokenIndex] ); return ( -
); diff --git a/frontend/src/components/annotator/sidebar/AnnotatorSidebar.tsx b/frontend/src/components/annotator/sidebar/AnnotatorSidebar.tsx index 4c64e792..90aebf9c 100644 --- a/frontend/src/components/annotator/sidebar/AnnotatorSidebar.tsx +++ b/frontend/src/components/annotator/sidebar/AnnotatorSidebar.tsx @@ -21,12 +21,8 @@ import { RelationItem } from "./RelationItem"; import "./AnnotatorSidebar.css"; import { useReactiveVar } from "@apollo/client"; import { - showSelectedAnnotationOnly, - showAnnotationBoundingBoxes, - showAnnotationLabels, openedCorpus, - openedDocument, - selectedAnalysis, + showStructuralAnnotations, } from "../../../graphql/cache"; import { AnalysisType, @@ -34,12 +30,12 @@ import { CorpusType, DatacellType, ExtractType, -} from "../../../graphql/types"; +} from "../../../types/graphql-api"; import { SearchSidebarWidget } from "../search_widget/SearchSidebarWidget"; import { FetchMoreOnVisible } from "../../widgets/infinite_scroll/FetchMoreOnVisible"; import useWindowDimensions from "../../hooks/WindowDimensionHook"; import { SingleDocumentExtractResults } from "../../extracts/SingleDocumentExtractResults"; -import { label_display_options, PermissionTypes } from "../../types"; +import { PermissionTypes } from "../../types"; import { getPermissions } from "../../../utils/transform"; import { PlaceholderCard } from "../../placeholders/PlaceholderCard"; import { CorpusStats } from "../../widgets/data-display/CorpusStatus"; @@ -143,6 +139,7 @@ const StyledTab = styled(Tab)` flex: 1; display: flex; flex-direction: column; + overflow-y: hidden; .ui.secondary.menu { justify-content: center; @@ -209,9 +206,8 @@ export const AnnotatorSidebar = ({ fetchMore?: () => void; }) => { const annotationStore = useContext(AnnotationStore); - const label_display_behavior = useReactiveVar(showAnnotationLabels); const opened_corpus = useReactiveVar(openedCorpus); - const opened_document = useReactiveVar(openedDocument); + const show_structural_annotations = useReactiveVar(showStructuralAnnotations); // Slightly kludgy way to handle responsive layout and drop sidebar once it becomes a pain // If there's enough interest to warrant a refactor, we can put some more thought into how @@ -226,13 +222,6 @@ export const AnnotatorSidebar = ({ (!selected_analysis && !selected_extract && !opened_corpus?.labelSet) ); - const show_selected_annotation_only = useReactiveVar( - showSelectedAnnotationOnly - ); - const show_annotation_bounding_boxes = useReactiveVar( - showAnnotationBoundingBoxes - ); - const [showCorpusStats, setShowCorpusStats] = useState(false); const [showSearchPane, setShowSearchPane] = useState(true); const [activeIndex, setActiveIndex] = useState(0); @@ -253,8 +242,6 @@ export const AnnotatorSidebar = ({ }; const { - showStructuralLabels, - toggleShowStructuralLabels, textSearchMatches, selectedRelations, pdfAnnotations, @@ -265,18 +252,29 @@ export const AnnotatorSidebar = ({ const relations = pdfAnnotations.relations; const filteredAnnotations = useMemo(() => { + let return_annotations = [...annotations]; + if (!show_structural_annotations) { + return_annotations = return_annotations.filter( + (annotation) => !annotation.structural + ); + } + if ( !annotationStore.showOnlySpanLabels || annotationStore.showOnlySpanLabels.length === 0 ) { - return annotations; + return return_annotations; } - return annotations.filter((annotation) => + return return_annotations.filter((annotation) => annotationStore.showOnlySpanLabels?.some( (label) => label.id === annotation.annotationLabel.id ) ); - }, [annotations, annotationStore.showOnlySpanLabels]); + }, [ + annotations, + annotationStore.showOnlySpanLabels, + show_structural_annotations, + ]); useEffect(() => { try { @@ -473,6 +471,7 @@ export const AnnotatorSidebar = ({ menuItem: "Search", render: () => ( diff --git a/frontend/src/components/annotator/sidebar/HighlightItem.tsx b/frontend/src/components/annotator/sidebar/HighlightItem.tsx index 2e60f599..506a0147 100644 --- a/frontend/src/components/annotator/sidebar/HighlightItem.tsx +++ b/frontend/src/components/annotator/sidebar/HighlightItem.tsx @@ -3,7 +3,7 @@ import { Label, Button, Popup, Icon, SemanticICONS } from "semantic-ui-react"; import styled from "styled-components"; import { Trash2, ArrowRight, ArrowLeft } from "lucide-react"; import { HorizontallyJustifiedDiv } from "./common"; -import { AnnotationStore, ServerAnnotation } from "../context"; +import { AnnotationStore, ServerTokenAnnotation } from "../context"; import { PermissionTypes } from "../../types"; interface HighlightContainerProps { @@ -29,7 +29,7 @@ const HighlightContainer = styled.div` const AnnotationLabel = styled(Label)` &&& { - background-color: ${(props) => props.color || "grey"}; + background-color: #${(props) => props.color || "grey"}; color: white; margin: 0 0.5rem 0.5rem 0; padding: 0.5em 0.8em; @@ -81,7 +81,7 @@ const LocationText = styled.div` `; interface HighlightItemProps { - annotation: ServerAnnotation; + annotation: ServerTokenAnnotation; className?: string; read_only: boolean; relations: Array<{ sourceIds: string[]; targetIds: string[] }>; @@ -100,13 +100,14 @@ export const HighlightItem: React.FC = ({ const annotationStore = useContext(AnnotationStore); const selected = annotationStore.selectedAnnotations.includes(annotation.id); + console.log("Selection element refs: ", annotationStore.selectionElementRefs); + const my_output_relationships = relations.filter((relation) => relation.sourceIds.includes(annotation.id) ); const my_input_relationships = relations.filter((relation) => relation.targetIds.includes(annotation.id) ); - console.log("Annotation over: ", annotation.rawText); return ( void; onSelectAnnotation: (annotationId: string) => void; onDeleteRelation: (relationId: string) => void; diff --git a/frontend/src/components/annotator/topbar/AnnotatorTopbar.tsx b/frontend/src/components/annotator/topbar/AnnotatorTopbar.tsx index c69d982d..b7879e99 100644 --- a/frontend/src/components/annotator/topbar/AnnotatorTopbar.tsx +++ b/frontend/src/components/annotator/topbar/AnnotatorTopbar.tsx @@ -8,7 +8,7 @@ import { CorpusType, DocumentType, ExtractType, -} from "../../../graphql/types"; +} from "../../../types/graphql-api"; import { ExtractAndAnalysisHorizontalSelector } from "../../analyses/AnalysisSelectorForCorpus"; import { useReactiveVar } from "@apollo/client"; import { setTopbarVisible } from "../../../graphql/cache"; diff --git a/frontend/src/components/annotator/topbar/SelectedAnalysisCard.tsx b/frontend/src/components/annotator/topbar/SelectedAnalysisCard.tsx index cb0df89f..2772c1c3 100644 --- a/frontend/src/components/annotator/topbar/SelectedAnalysisCard.tsx +++ b/frontend/src/components/annotator/topbar/SelectedAnalysisCard.tsx @@ -1,5 +1,5 @@ import { Card, Image } from "semantic-ui-react"; -import { AnalysisType } from "../../../graphql/types"; +import { AnalysisType } from "../../../types/graphql-api"; export const SelectedAnalysisCard = () => { return ( diff --git a/frontend/src/components/annotator/utils.ts b/frontend/src/components/annotator/utils.ts index 5f0b579d..3b7a1cd0 100644 --- a/frontend/src/components/annotator/utils.ts +++ b/frontend/src/components/annotator/utils.ts @@ -12,7 +12,7 @@ import { PDFPageInfo, RelationGroup, TokenId, - ServerAnnotation, + ServerTokenAnnotation, } from "./context"; import { Token } from "../types"; import { @@ -52,8 +52,8 @@ export function getRelationImageHref(type: string): string { } export function annotationSelectedViaRelationship( - this_annotation: ServerAnnotation, - annotations: ServerAnnotation[], + this_annotation: ServerTokenAnnotation, + annotations: ServerTokenAnnotation[], relation: RelationGroup ): "SOURCE" | "TARGET" | "" { // console.log("this_annotation", this_annotation); diff --git a/frontend/src/components/corpuses/CorpusCards.tsx b/frontend/src/components/corpuses/CorpusCards.tsx index aa3d0302..7985de65 100644 --- a/frontend/src/components/corpuses/CorpusCards.tsx +++ b/frontend/src/components/corpuses/CorpusCards.tsx @@ -15,7 +15,7 @@ import { import { Card, Dimmer, Loader } from "semantic-ui-react"; import { PlaceholderCard } from "../placeholders/PlaceholderCard"; -import { CorpusType, PageInfo } from "../../graphql/types"; +import { CorpusType, PageInfo } from "../../types/graphql-api"; import { StartForkCorpusInput, StartForkCorpusOutput, diff --git a/frontend/src/components/corpuses/CorpusDashboard.tsx b/frontend/src/components/corpuses/CorpusDashboard.tsx index bdd9430a..39999080 100644 --- a/frontend/src/components/corpuses/CorpusDashboard.tsx +++ b/frontend/src/components/corpuses/CorpusDashboard.tsx @@ -23,7 +23,7 @@ import { GetCorpusStatsOutputType, } from "../../graphql/queries"; import CountUp from "react-countup"; -import { CorpusType } from "../../graphql/types"; +import { CorpusType } from "../../types/graphql-api"; import useWindowDimensions from "../hooks/WindowDimensionHook"; import { MOBILE_VIEW_BREAKPOINT } from "../../assets/configurations/constants"; diff --git a/frontend/src/components/corpuses/CorpusItem.tsx b/frontend/src/components/corpuses/CorpusItem.tsx index e81c1ceb..63a4a71a 100644 --- a/frontend/src/components/corpuses/CorpusItem.tsx +++ b/frontend/src/components/corpuses/CorpusItem.tsx @@ -10,12 +10,12 @@ import { Icon, Label, Header, + MenuItemProps, } from "semantic-ui-react"; import _ from "lodash"; import styled from "styled-components"; -import { LabelSetStatistic } from "../widgets/data-display/LabelSetStatisticWidget"; -import { CorpusType } from "../../graphql/types"; +import { CorpusType } from "../../types/graphql-api"; import default_corpus_icon from "../../assets/images/defaults/default_corpus.png"; import { getPermissions } from "../../utils/transform"; import { PermissionTypes } from "../types"; @@ -154,7 +154,7 @@ export const CorpusItem: React.FC = ({ item.myPermissions ? item.myPermissions : [] ); - let context_menus: React.ReactNode[] = []; + let context_menus: MenuItemProps[] = []; if (analyzers_available) { context_menus.push({ @@ -316,12 +316,16 @@ export const CorpusItem: React.FC = ({ open={contextMenuOpen === id} hideOnScroll > - setContextMenuOpen(-1)} - secondary - vertical - /> + + {context_menus.map((item) => ( + setContextMenuOpen(-1)} + /> + ))} + ); diff --git a/frontend/src/components/corpuses/CorpusSelector.tsx b/frontend/src/components/corpuses/CorpusSelector.tsx index b2e91ef4..edffb99b 100644 --- a/frontend/src/components/corpuses/CorpusSelector.tsx +++ b/frontend/src/components/corpuses/CorpusSelector.tsx @@ -13,7 +13,7 @@ import { import styled from "styled-components"; import _ from "lodash"; -import { CorpusType } from "../../graphql/types"; +import { CorpusType } from "../../types/graphql-api"; import { getPermissions } from "../../utils/transform"; import { PermissionTypes } from "../types"; import { GetCorpusesInputs, GetCorpusesOutputs } from "../../graphql/queries"; diff --git a/frontend/src/components/documents/CorpusDocumentCards.tsx b/frontend/src/components/documents/CorpusDocumentCards.tsx index aa3f0414..20ab0582 100644 --- a/frontend/src/components/documents/CorpusDocumentCards.tsx +++ b/frontend/src/components/documents/CorpusDocumentCards.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; import _ from "lodash"; import { toast } from "react-toastify"; import { useMutation, useQuery, useReactiveVar } from "@apollo/client"; @@ -13,6 +13,8 @@ import { filterToLabelId, selectedMetaAnnotationId, openedDocument, + showUploadNewDocumentsModal, + uploadModalPreloadedFiles, } from "../../graphql/cache"; import { REMOVE_DOCUMENTS_FROM_CORPUS, @@ -24,7 +26,8 @@ import { RequestDocumentsOutputs, GET_DOCUMENTS, } from "../../graphql/queries"; -import { DocumentType } from "../../graphql/types"; +import { DocumentType } from "../../types/graphql-api"; +import { FileUploadPackageProps } from "../widgets/modals/DocumentUploadModal"; export const CorpusDocumentCards = ({ opened_corpus_id, @@ -171,6 +174,20 @@ export const CorpusDocumentCards = ({ openedDocument(document); }; + const onDrop = useCallback((acceptedFiles: File[]) => { + const filePackages: FileUploadPackageProps[] = acceptedFiles.map( + (file) => ({ + file, + formData: { + title: file.name, + description: `Content summary for ${file.name}`, + }, + }) + ); + showUploadNewDocumentsModal(true); + uploadModalPreloadedFiles(filePackages); + }, []); + return ( ); }; diff --git a/frontend/src/components/documents/DocumentCards.tsx b/frontend/src/components/documents/DocumentCards.tsx index 1053007e..9b3a89aa 100644 --- a/frontend/src/components/documents/DocumentCards.tsx +++ b/frontend/src/components/documents/DocumentCards.tsx @@ -1,11 +1,12 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; import { Card, Dimmer, Loader } from "semantic-ui-react"; +import { useDropzone } from "react-dropzone"; import _ from "lodash"; import { DocumentItem } from "./DocumentItem"; import { PlaceholderCard } from "../placeholders/PlaceholderCard"; -import { DocumentType, PageInfo } from "../../graphql/types"; +import { DocumentType, PageInfo } from "../../types/graphql-api"; import { FetchMoreOnVisible } from "../widgets/infinite_scroll/FetchMoreOnVisible"; import useWindowDimensions from "../hooks/WindowDimensionHook"; import { determineCardColCount } from "../../utils/layout"; @@ -13,6 +14,7 @@ import { MOBILE_VIEW_BREAKPOINT } from "../../assets/configurations/constants"; interface DocumentCardProps { style?: Record; + containerStyle?: React.CSSProperties; // New prop for outer container items: DocumentType[]; pageInfo: PageInfo | undefined; loading: boolean; @@ -21,9 +23,12 @@ interface DocumentCardProps { onClick?: (document: DocumentType) => void; removeFromCorpus?: (doc_ids: string[]) => void | any; fetchMore: (args?: any) => void | any; + onDrop: (acceptedFiles: File[]) => void; + corpusId: string | null; } export const DocumentCards = ({ + containerStyle, style, items, pageInfo, @@ -33,6 +38,8 @@ export const DocumentCards = ({ onClick, removeFromCorpus, fetchMore, + onDrop, + corpusId, }: DocumentCardProps) => { const { width } = useWindowDimensions(); const use_mobile_layout = width <= MOBILE_VIEW_BREAKPOINT; @@ -104,17 +111,53 @@ export const DocumentCards = ({ /** * Return DocumentItems */ + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + noClick: true, + noKeyboard: true, + }); + return ( - <> +
+ + {isDragActive && ( +
+
+ Drop files here to upload +
+
+ )}
@@ -123,6 +166,6 @@ export const DocumentCards = ({
- +
); }; diff --git a/frontend/src/components/documents/DocumentItem.tsx b/frontend/src/components/documents/DocumentItem.tsx index 2070d2a8..20d92dbf 100644 --- a/frontend/src/components/documents/DocumentItem.tsx +++ b/frontend/src/components/documents/DocumentItem.tsx @@ -20,7 +20,7 @@ import { showDeleteDocumentsModal, viewingDocument, } from "../../graphql/cache"; -import { AnnotationLabelType, DocumentType } from "../../graphql/types"; +import { AnnotationLabelType, DocumentType } from "../../types/graphql-api"; import { downloadFile } from "../../utils/files"; import fallback_doc_icon from "../../assets/images/defaults/default_doc_icon.jpg"; import { getPermissions } from "../../utils/transform"; @@ -141,6 +141,7 @@ export const DocumentItem: React.FC = ({ backendLock, isPublic, myPermissions, + fileType, } = item; const cardClickHandler = ( @@ -179,7 +180,7 @@ export const DocumentItem: React.FC = ({ item.myPermissions ? item.myPermissions : [] ); - let context_menus: React.ReactNode[] = []; + let context_menus: ContextMenuItem[] = []; if (my_permissions.includes(PermissionTypes.CAN_REMOVE)) { context_menus.push({ key: "delete", @@ -290,7 +291,9 @@ export const DocumentItem: React.FC = ({ ) : null} - {`Document Type: *.pdf`} + + Document Type: + Description: {description} @@ -318,14 +321,27 @@ export const DocumentItem: React.FC = ({ open={contextMenuOpen === id} hideOnScroll > - setContextMenuOpen(-1)} - secondary - vertical - /> + + {context_menus.map((item) => ( + { + item.onClick(); + setContextMenuOpen(-1); + }} + /> + ))} + ); }; + +interface ContextMenuItem { + key: string; + icon: string; + content: string; + onClick: () => void; +} diff --git a/frontend/src/components/exports/ExportItemRow.tsx b/frontend/src/components/exports/ExportItemRow.tsx index 7ab048b5..ccdd0598 100644 --- a/frontend/src/components/exports/ExportItemRow.tsx +++ b/frontend/src/components/exports/ExportItemRow.tsx @@ -1,5 +1,5 @@ import { Table, Icon, Button } from "semantic-ui-react"; -import { ExportObject } from "../../graphql/types"; +import { ExportObject } from "../../types/graphql-api"; import { DateTimeWidget } from "../widgets/data-display/DateTimeWidget"; interface ExportItemRowProps { diff --git a/frontend/src/components/exports/ExportList.tsx b/frontend/src/components/exports/ExportList.tsx index 29aff343..ccc5f7b9 100644 --- a/frontend/src/components/exports/ExportList.tsx +++ b/frontend/src/components/exports/ExportList.tsx @@ -1,6 +1,6 @@ import { Table, Dimmer, Loader } from "semantic-ui-react"; -import { ExportObject } from "../../graphql/types"; -import { PageInfo } from "../../graphql/types"; +import { ExportObject } from "../../types/graphql-api"; +import { PageInfo } from "../../types/graphql-api"; import { FetchMoreOnVisible } from "../widgets/infinite_scroll/FetchMoreOnVisible"; import { ExportItemRow } from "./ExportItemRow"; diff --git a/frontend/src/components/extracts/ExtractCards.tsx b/frontend/src/components/extracts/ExtractCards.tsx index 5d9d66c3..d887378f 100644 --- a/frontend/src/components/extracts/ExtractCards.tsx +++ b/frontend/src/components/extracts/ExtractCards.tsx @@ -2,7 +2,7 @@ import { Card, Dimmer, Loader } from "semantic-ui-react"; import { ExtractItem } from "./ExtractItem"; import { PlaceholderCard } from "../placeholders/PlaceholderCard"; import { FetchMoreOnVisible } from "../widgets/infinite_scroll/FetchMoreOnVisible"; -import { ExtractType, CorpusType, PageInfo } from "../../graphql/types"; +import { ExtractType, CorpusType, PageInfo } from "../../types/graphql-api"; import { useReactiveVar } from "@apollo/client"; import { openedExtract, selectedExtractIds } from "../../graphql/cache"; import useWindowDimensions from "../hooks/WindowDimensionHook"; diff --git a/frontend/src/components/extracts/ExtractItem.tsx b/frontend/src/components/extracts/ExtractItem.tsx index d6814a51..79f9f34a 100644 --- a/frontend/src/components/extracts/ExtractItem.tsx +++ b/frontend/src/components/extracts/ExtractItem.tsx @@ -7,7 +7,7 @@ import { REQUEST_DELETE_EXTRACT, } from "../../graphql/mutations"; import { GetExtractsOutput, GET_EXTRACTS } from "../../graphql/queries"; -import { ExtractType, CorpusType } from "../../graphql/types"; +import { ExtractType, CorpusType } from "../../types/graphql-api"; import _ from "lodash"; import { PermissionTypes } from "../types"; diff --git a/frontend/src/components/extracts/SingleDocumentExtractResults.tsx b/frontend/src/components/extracts/SingleDocumentExtractResults.tsx index f93c977c..3a0cc11c 100644 --- a/frontend/src/components/extracts/SingleDocumentExtractResults.tsx +++ b/frontend/src/components/extracts/SingleDocumentExtractResults.tsx @@ -13,7 +13,7 @@ import { ColumnType, DatacellType, ServerAnnotationType, -} from "../../graphql/types"; +} from "../../types/graphql-api"; import { useReactiveVar, useMutation } from "@apollo/client"; import { onlyDisplayTheseAnnotations, @@ -21,7 +21,7 @@ import { showAnnotationBoundingBoxes, showAnnotationLabels, } from "../../graphql/cache"; -import { LabelDisplayBehavior } from "../../graphql/types"; +import { LabelDisplayBehavior } from "../../types/graphql-api"; import { toast } from "react-toastify"; import { REQUEST_APPROVE_DATACELL, diff --git a/frontend/src/components/extracts/datagrid/DataCell.tsx b/frontend/src/components/extracts/datagrid/DataCell.tsx index 8cff4510..85c03763 100644 --- a/frontend/src/components/extracts/datagrid/DataCell.tsx +++ b/frontend/src/components/extracts/datagrid/DataCell.tsx @@ -5,7 +5,7 @@ import { DatacellType, LabelDisplayBehavior, ServerAnnotationType, -} from "../../../graphql/types"; +} from "../../../types/graphql-api"; import { JSONTree } from "react-json-tree"; import { displayAnnotationOnAnnotatorLoad, @@ -126,15 +126,40 @@ export const ExtractDatacell = ({ }, [cellData]); const renderJsonPreview = (data: Record) => { - const jsonString = JSON.stringify(data?.data ? data.data : {}, null, 2); - const preview = jsonString.split("\n").slice(0, 3).join("\n") + "\n..."; - return ( - {preview}} - content={
{jsonString}
} - wide="very" - /> - ); + // Handle empty or invalid data + if (!data || !data.data || Object.keys(data.data).length === 0) { + return ( + -} + content={
{"{}"}
} + wide="very" + /> + ); + } + + try { + const jsonString = JSON.stringify(data.data, null, 2); + const previewLines = jsonString.split("\n").slice(0, 3); + const preview = + previewLines.join("\n") + + (jsonString.split("\n").length > 3 ? "\n..." : ""); + + return ( + {preview || "-"}} + content={
{jsonString}
} + wide="very" + /> + ); + } catch (e) { + return ( + -} + content={
{"{}"}
} + wide="very" + /> + ); + } }; return ( @@ -143,54 +168,55 @@ export const ExtractDatacell = ({ {cellData.started && !cellData.completed && !cellData.failed ? ( ) : ( - <> +
+ {renderJsonPreview( + cellData?.data ? { data: cellData.data || {} } : { data: {} } + )} + {!readOnly && ( +
+ } + content={ + +
+ )} +
)} -
- {renderJsonPreview(cellData?.data ?? {})} - {!readOnly && ( -
- } - content={ - -
- )} -
Edit Data diff --git a/frontend/src/components/extracts/datagrid/DataGrid.tsx b/frontend/src/components/extracts/datagrid/DataGrid.tsx index a4aa8d21..3bbaf90b 100644 --- a/frontend/src/components/extracts/datagrid/DataGrid.tsx +++ b/frontend/src/components/extracts/datagrid/DataGrid.tsx @@ -1,27 +1,19 @@ -import React, { useEffect, useState } from "react"; -import { - Table, - Button, - Icon, - Dropdown, - Segment, - Dimmer, - Loader, -} from "semantic-ui-react"; -import { - ColumnType, - DatacellType, - DocumentType, - ExtractType, -} from "../../../graphql/types"; -import { SelectDocumentsModal } from "../../widgets/modals/SelectDocumentsModal"; -import { - addingColumnToExtract, - editingColumnForExtract, -} from "../../../graphql/cache"; -import { EmptyDatacell } from "./EmptyDataCell"; -import { ExtractDatacell } from "./DataCell"; +import React, { + useCallback, + useMemo, + useState, + useRef, + useEffect, +} from "react"; +import DataGrid, { + RowsChangeData, + CopyEvent, + PasteEvent, + SelectColumn, +} from "react-data-grid"; import { useMutation } from "@apollo/client"; +import { toast } from "react-toastify"; +import { Button, Icon, Popup } from "semantic-ui-react"; import { REQUEST_APPROVE_DATACELL, REQUEST_EDIT_DATACELL, @@ -32,297 +24,1255 @@ import { RequestEditDatacellOutputType, RequestRejectDatacellInputType, RequestRejectDatacellOutputType, + REQUEST_UPDATE_COLUMN, + RequestUpdateColumnInputType, + RequestUpdateColumnOutputType, + REQUEST_DELETE_COLUMN, + RequestDeleteColumnInputType, + RequestDeleteColumnOutputType, } from "../../../graphql/mutations"; -import { toast } from "react-toastify"; +import { ExtractCellFormatter } from "./ExtractCellFormatter"; +import { + ExtractGridColumn, + ExtractGridRow, + CellStatus, +} from "../../../types/extract-grid"; +import { + ColumnType, + DatacellType, + DocumentType, + ExtractType, +} from "../../../types/graphql-api"; +import "react-data-grid/lib/styles.css"; +import { useDropzone } from "react-dropzone"; +import { UPLOAD_DOCUMENT } from "../../../graphql/mutations"; +import { Dimmer, Loader } from "semantic-ui-react"; +import { parseOutputType } from "../../../utils/parseOutputType"; +import { JSONSchema7 } from "json-schema"; +import { TruncatedText } from "../../widgets/data-display/TruncatedText"; +import { CreateColumnModal } from "../../widgets/modals/CreateColumnModal"; +import { SelectDocumentsModal } from "../../widgets/modals/SelectDocumentsModal"; + +interface DragState { + isDragging: boolean; + dragY: number | null; +} interface DataGridProps { extract: ExtractType; cells: DatacellType[]; rows: DocumentType[]; + columns: ColumnType[]; onAddDocIds: (extractId: string, documentIds: string[]) => void; onRemoveDocIds: (extractId: string, documentIds: string[]) => void; onRemoveColumnId: (columnId: string) => void; - columns: ColumnType[]; + onUpdateRow?: (newRow: DocumentType) => void; + onAddColumn: () => void; + loading?: boolean; +} + +// Add these styles near the top of the file +const styles = { + gridWrapper: { + height: "100%", + width: "100%", + position: "relative" as const, + backgroundColor: "#fff", + borderRadius: "8px", + boxShadow: "0 2px 8px rgba(0,0,0,0.05)", + minHeight: "400px", + display: "flex", + flexDirection: "column" as const, + border: "1px solid #e0e0e0", + }, + headerCell: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "8px 16px", + backgroundColor: "#f8f9fa", + borderBottom: "2px solid #e9ecef", + fontWeight: 600, + }, + phantomColumn: { + position: "absolute" as const, + right: 0, + top: 0, + bottom: 0, + width: "60px", + cursor: "pointer", + border: "2px dashed #dee2e6", + borderLeft: "none", + background: "#fff", + transition: "all 0.2s ease", + display: "flex", + alignItems: "center", + justifyContent: "center", + "&:hover": { + background: "#f8f9fa", + borderColor: "#4caf50", + }, + }, + dropOverlay: { + position: "absolute" as const, + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 120, 255, 0.05)", + backdropFilter: "blur(2px)", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: 999, + pointerEvents: "none" as const, + }, + dropMessage: { + padding: "20px 30px", + backgroundColor: "white", + borderRadius: "8px", + boxShadow: "0 4px 20px rgba(0,0,0,0.1)", + border: "2px dashed #0078ff", + fontSize: "1.1em", + }, +}; + +// Add new interfaces for filter state +interface ColumnFilter { + value: string; + enabled: boolean; +} + +interface FilterState { + [columnId: string]: ColumnFilter; } -export const DataGrid = ({ +export const ExtractDataGrid: React.FC = ({ extract, - cells, + cells: initialCells, rows, columns, onAddDocIds, onRemoveDocIds, onRemoveColumnId, -}: DataGridProps) => { - const [lastCells, setLastCells] = useState(cells); - const [isAddingColumn, setIsAddingColumn] = useState(false); - const [showAddRowButton, setShowAddRowButton] = useState(false); - const [openAddRowModal, setOpenAddRowModal] = useState(false); + onUpdateRow, + onAddColumn, + loading, +}) => { + console.log("ExtractDataGrid received columns:", columns); + console.log("ExtractDataGrid received extract:", extract); + console.log("ExtractDataGrid received rows:", rows); + + const [selectedRows, setSelectedRows] = useState>(new Set()); + const [isDeleting, setIsDeleting] = useState(false); + + console.log("Cells", initialCells); + + useEffect(() => { + console.log("Cells", initialCells); + console.log("Columns", columns); + }, [initialCells, columns]); + + const [dragState, setDragState] = useState({ + isDragging: false, + dragY: null, + }); + const gridRef = useRef(null); + + // Local state for cells + const [localCells, setLocalCells] = useState(initialCells); + // Add an effect to update localCells when initialCells changes useEffect(() => { - setLastCells(cells); - }, [cells]); + console.log("Initial cells received:", initialCells); + setLocalCells(initialCells); + }, [initialCells]); + + // Also add this debug log + useEffect(() => { + console.log("Local cells state:", localCells); + }, [localCells]); + + // Add a function to derive cell status from a cell + const deriveCellStatus = useCallback( + (cell: DatacellType): CellStatus => { + const extractIsProcessing = + extract.started && !extract.finished && !extract.error; + const cellIsProcessing = cell.started && !cell.completed && !cell.failed; + const isProcessing = + cellIsProcessing || (extractIsProcessing && !cell.started); + + return { + isLoading: Boolean(isProcessing), + isApproved: Boolean(cell.approvedBy), + isRejected: Boolean(cell.rejectedBy), + isEdited: Boolean(cell.correctedData), + originalData: cell.data || null, + correctedData: cell.correctedData || null, + error: cell.failed || null, + }; + }, + [extract] + ); + + // Check if extract is complete + const isExtractComplete = + extract.started && extract.finished && !extract.error; + + // Convert data to grid format + const gridRows = useMemo(() => { + if (!rows || rows.length === 0) { + return [ + { + id: "placeholder", + documentId: "", + documentTitle: "Drop PDF documents here or click to upload", + ...columns.reduce((acc, col) => { + acc[col.id] = ""; + return acc; + }, {} as Record), + }, + ]; + } + + console.log("Creating gridRows with:", { + rows, + cells: initialCells, + columns, + extract, + }); + return rows.map((row) => { + const rowData: ExtractGridRow = { + id: row.id, + documentId: row.id, + documentTitle: row.title || "", + }; + + console.log("Processing row:", row.id); + + columns.forEach((column) => { + console.log("Processing column for row:", { + rowId: row.id, + columnId: column.id, + }); + const cell = initialCells.find( + (c) => c.document.id === row.id && c.column.id === column.id + ); + console.log("Found cell:", cell); + + if (cell) { + console.log("Cell data:", cell.data?.data); + rowData[column.id] = cell.data?.data || ""; // Ensure empty string instead of empty object + } else { + rowData[column.id] = ""; // Ensure empty string instead of empty object + } + }); + + console.log("Final rowData:", rowData); + return rowData; + }); + }, [rows, initialCells, columns, extract]); + + // Column Actions Component + const ColumnActions: React.FC<{ column: ExtractGridColumn }> = ({ + column, + }) => { + if (column.key === "documentTitle") return null; - const [requestApprove, { loading: trying_approve }] = useMutation< + return ( + + } + content={ + + + )} + + row.id} + selectedRows={selectedRows} + onSelectedRowsChange={setSelectedRows} + isRowSelectionDisabled={(row) => + Boolean(extract.started) || row.id === "placeholder" + } + className="custom-data-grid" + onRowsChange={onRowsChange} + onCopy={handleCopy} + onPaste={handlePaste} + headerRowHeight={filtersEnabled ? 70 : undefined} + /> + + {!extract.started && ( + + + + ); + + const renderPrimitiveEditor = () => { + const { type } = schema; + + switch (type) { + case "string": + return ( + + ); + + case "number": + return ( + + ); + + case "boolean": + return ( + + ); + + default: + return ( + + ); + } + }; + + if (schema.type === "object" || extractIsList) { + return ( + <> + + + + + )} + + ); +}; diff --git a/frontend/src/components/extracts/list/ExtractList.tsx b/frontend/src/components/extracts/list/ExtractList.tsx index 93384d63..95e1ab05 100644 --- a/frontend/src/components/extracts/list/ExtractList.tsx +++ b/frontend/src/components/extracts/list/ExtractList.tsx @@ -1,7 +1,7 @@ import { Table, Dimmer, Loader } from "semantic-ui-react"; import { ExtractItemRow } from "./ExtractListItem"; import { FetchMoreOnVisible } from "../../widgets/infinite_scroll/FetchMoreOnVisible"; -import { ExtractType, PageInfo } from "../../../graphql/types"; +import { ExtractType, PageInfo } from "../../../types/graphql-api"; interface ExtractListProps { items: ExtractType[] | undefined; diff --git a/frontend/src/components/extracts/list/ExtractListItem.tsx b/frontend/src/components/extracts/list/ExtractListItem.tsx index 23c6a4e9..81654025 100644 --- a/frontend/src/components/extracts/list/ExtractListItem.tsx +++ b/frontend/src/components/extracts/list/ExtractListItem.tsx @@ -1,5 +1,5 @@ import { Table, Icon, Button } from "semantic-ui-react"; -import { ExtractType } from "../../../graphql/types"; +import { ExtractType } from "../../../types/graphql-api"; import { DateTimeWidget } from "../../widgets/data-display/DateTimeWidget"; interface ExtractItemRowProps { diff --git a/frontend/src/components/labelsets/AnnotationLabelCard.tsx b/frontend/src/components/labelsets/AnnotationLabelCard.tsx index e4d5f796..78109838 100644 --- a/frontend/src/components/labelsets/AnnotationLabelCard.tsx +++ b/frontend/src/components/labelsets/AnnotationLabelCard.tsx @@ -19,7 +19,7 @@ import _ from "lodash"; import { IconDropdown } from "../widgets/icon-picker/index"; import { VerticallyCenteredDiv } from "../common"; import { ColorPickerSegment } from "../widgets/color-picker/ColorPickerSegment"; -import { AnnotationLabelType } from "../../graphql/types"; +import { AnnotationLabelType } from "../../types/graphql-api"; import { UpdateAnnotationLabelInputs } from "../../graphql/mutations"; import { getPermissions } from "../../utils/transform"; import { PermissionTypes } from "../types"; diff --git a/frontend/src/components/labelsets/AnnotationLabelItem.tsx b/frontend/src/components/labelsets/AnnotationLabelItem.tsx index 0219c94a..ecf99a00 100644 --- a/frontend/src/components/labelsets/AnnotationLabelItem.tsx +++ b/frontend/src/components/labelsets/AnnotationLabelItem.tsx @@ -3,7 +3,7 @@ import _ from "lodash"; import { Card, Popup, Image, Icon, Statistic, Menu } from "semantic-ui-react"; import default_icon from "../../assets/images/defaults/default_tag.png"; -import { LabelSetType } from "../../graphql/types"; +import { LabelSetType } from "../../types/graphql-api"; import { getPermissions } from "../../utils/transform"; import { PermissionTypes } from "../types"; import { MyPermissionsIndicator } from "../widgets/permissions/MyPermissionsIndicator"; @@ -19,6 +19,13 @@ interface AnnotationLabelItemProps { setContextMenuOpen: (args: any) => void | any; } +interface ContextMenuItem { + key: string; + content: string; + icon: string; + onClick: () => void; +} + const AnnotationLabelItem = ({ item, selected, @@ -95,10 +102,10 @@ const AnnotationLabelItem = ({ item.myPermissions ? item.myPermissions : [] ); - let context_menus: React.ReactNode[] = []; + let context_menus: ContextMenuItem[] = []; if (my_permissions.includes(PermissionTypes.CAN_REMOVE)) { context_menus.push({ - key: "copy", + key: "delete", content: "Delete Item", icon: "trash", onClick: () => onDelete(id), @@ -174,12 +181,19 @@ const AnnotationLabelItem = ({ open={contextMenuOpen === id} hideOnScroll > - setContextMenuOpen(-1)} - secondary - vertical - /> + + {context_menus.map((item) => ( + { + item.onClick(); + setContextMenuOpen(-1); + }} + /> + ))} + ); diff --git a/frontend/src/components/labelsets/LabelSetCards.tsx b/frontend/src/components/labelsets/LabelSetCards.tsx index 97d2048e..d55fa792 100644 --- a/frontend/src/components/labelsets/LabelSetCards.tsx +++ b/frontend/src/components/labelsets/LabelSetCards.tsx @@ -10,7 +10,7 @@ import { openedLabelset, selectedLabelsetIds, } from "../../graphql/cache"; -import { LabelSetType, PageInfo } from "../../graphql/types"; +import { LabelSetType, PageInfo } from "../../types/graphql-api"; import { FetchMoreOnVisible } from "../widgets/infinite_scroll/FetchMoreOnVisible"; import useWindowDimensions from "../hooks/WindowDimensionHook"; import { determineCardColCount } from "../../utils/layout"; diff --git a/frontend/src/components/labelsets/LabelSetEditModal.tsx b/frontend/src/components/labelsets/LabelSetEditModal.tsx index 37070bbf..f79c7223 100644 --- a/frontend/src/components/labelsets/LabelSetEditModal.tsx +++ b/frontend/src/components/labelsets/LabelSetEditModal.tsx @@ -1,22 +1,19 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { Button, Modal, Header, Icon, Card, - Segment, Tab, Dimmer, Loader, TabProps, + Message, } from "semantic-ui-react"; - import _ from "lodash"; import Fuse from "fuse.js"; - import { AnnotationLabelCard } from "./AnnotationLabelCard"; - import { CRUDWidget } from "../widgets/CRUD/CRUDWidget"; import { useQuery, useMutation, useReactiveVar } from "@apollo/client"; import { @@ -38,31 +35,28 @@ import { CreateAnnotationLabelForLabelsetInputs, CREATE_ANNOTATION_LABEL_FOR_LABELSET, } from "../../graphql/mutations"; -import { HorizontallyCenteredDiv } from "../layout/Wrappers"; import { CreateAndSearchBar, DropdownActionProps, } from "../layout/CreateAndSearchBar"; import { openedLabelset } from "../../graphql/cache"; - import { AnnotationLabelType, LabelSetType, LabelType, -} from "../../graphql/types"; +} from "../../types/graphql-api"; import { newLabelSetForm_Schema, newLabelSetForm_Ui_Schema, } from "../forms/schemas"; - import { toast } from "react-toastify"; import { getPermissions } from "../../utils/transform"; import { PermissionTypes } from "../types"; +import styled from "styled-components"; const fuse_options = { includeScore: false, findAllMatches: true, - // Search in `label` and in `description` fields keys: ["label", "description"], }; @@ -71,13 +65,73 @@ interface LabelSetEditModalProps { toggleModal: () => any; } +const StyledModal = styled(Modal)` + &&& { + max-width: 90vw; + width: 1200px; + border-radius: 12px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); + } +`; + +const ModalContent = styled(Modal.Content)` + &&& { + padding: 2rem; + } +`; + +const TabContainer = styled(Tab)` + &&& { + height: 60vh; + overflow: hidden; + display: flex; + flex-direction: column; + } +`; + +const TabPane = styled(Tab.Pane)` + &&& { + flex: 1; + overflow-y: auto; + padding: 1rem; + max-width: 100%; + } +`; + +const SearchBarContainer = styled.div` + margin-bottom: 1rem; +`; + +const CardGroup = styled(Card.Group)` + &&& { + margin-top: 1rem; + width: 100%; + } +`; + +const EmptyStateMessage = styled(Message)` + &&& { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + text-align: center; + } +`; + +const ScrollableTabPane = styled(TabPane)` + &&& { + overflow-y: auto; + max-height: calc(60vh - 2rem); // Adjust this value as needed + } +`; + export const LabelSetEditModal = ({ open, toggleModal, }: LabelSetEditModalProps) => { const opened_labelset = useReactiveVar(openedLabelset); - console.log("Opened labelset", opened_labelset); - const [searchTerm, setSearchTerm] = useState(""); const [activeIndex, setActiveIndex] = useState(0); const [updatedObject, setUpdatedObject] = useState({}); @@ -90,24 +144,23 @@ export const LabelSetEditModal = ({ let my_permissions = getPermissions( opened_labelset?.myPermissions ? opened_labelset.myPermissions : [] ); - console.log("my_permissions", my_permissions); - const [createAnnotationLabelForLabelset, {}] = useMutation< + const [createAnnotationLabelForLabelset] = useMutation< CreateAnnotationLabelForLabelsetOutputs, CreateAnnotationLabelForLabelsetInputs >(CREATE_ANNOTATION_LABEL_FOR_LABELSET); - const [ - mutateLabelset, - { data: create_data, loading: create_loading, error: create_error }, - ] = useMutation(CREATE_LABELSET); + const [mutateLabelset, { loading: create_loading }] = useMutation< + CreateLabelsetOutputs, + CreateLabelsetInputs + >(CREATE_LABELSET); - const [mutateAnnotationLabel, {}] = useMutation< + const [mutateAnnotationLabel] = useMutation< UpdateAnnotationLabelOutputs, UpdateAnnotationLabelInputs >(UPDATE_ANNOTATION_LABEL); - const [deleteMultipleLabels, {}] = useMutation< + const [deleteMultipleLabels] = useMutation< DeleteMultipleAnnotationLabelOutputs, DeleteMultipleAnnotationLabelInputs >(DELETE_MULTIPLE_ANNOTATION_LABELS); @@ -117,57 +170,30 @@ export const LabelSetEditModal = ({ loading: label_set_loading, error: label_set_fetch_error, data: label_set_data, - fetchMore, } = useQuery( GET_LABELSET_WITH_ALL_LABELS, { variables: { id: opened_labelset?.id ? opened_labelset.id : "", }, - notifyOnNetworkStatusChange: true, // required to get loading signal on fetchMore + notifyOnNetworkStatusChange: true, } ); if (label_set_fetch_error || label_set_loading) { return ( - toggleModal()} size="large"> - - {}} - actions={[]} - placeholder="Search for label by description or name..." - value={""} - /> - - - {label_set_loading || create_loading ? ( - - - - ) : ( - <> - )} - - - - {canSave ? ( - - ) : ( - <> - )} - - + toggleModal()}> + + + + + + ); } - // console.log("LabelSetEditModal - labelset data", label_set_data); - const labels: AnnotationLabelType[] = label_set_data?.labelset ?.allAnnotationLabels ? (label_set_data.labelset.allAnnotationLabels.filter( @@ -180,7 +206,6 @@ export const LabelSetEditModal = ({ variables: { labelIdsToDelete: labels.map((label) => label.id) }, }) .then((data) => { - // console.log("Success deleting labels!", data); refetch(); }) .catch((err) => { @@ -264,6 +289,23 @@ export const LabelSetEditModal = ({ }); }; + const handleCreateSpanLabel = () => { + createAnnotationLabelForLabelset({ + variables: { + color: "00000", + description: "New span label", + icon: "tag", + text: "New Span Label", + labelType: LabelType.SpanLabel, + labelsetId: opened_labelset?.id ? opened_labelset.id : "", + }, + }) + .then(() => refetch()) + .catch((err) => { + console.log("Error trying to create span label: ", err); + }); + }; + const updateLabelSet = (obj: LabelSetType) => { mutateLabelset({ variables: { ...obj } }) .then(() => refetch()) @@ -273,7 +315,6 @@ export const LabelSetEditModal = ({ }; const updateLabel = (obj: UpdateAnnotationLabelInputs) => { - // console.log("Update label", obj); mutateAnnotationLabel({ variables: { ...obj } }) .then((data) => { refetch(); @@ -284,11 +325,8 @@ export const LabelSetEditModal = ({ }; const onCRUDChange = (labelsetData: LabelSetType) => { - // console.log("On CRUD Change", onCRUDChange); setChangedValues({ ...changedValues, ...labelsetData }); - // console.log("changedValues", changedValues); setUpdatedObject({ ...opened_labelset, ...updatedObject, ...labelsetData }); - // console.log("Updated object", updatedObject); setCanSave(true); }; @@ -298,7 +336,6 @@ export const LabelSetEditModal = ({ ) => setActiveIndex(data?.activeIndex ? data.activeIndex : 0); const handleSave = () => { - // console.log("Handle save", {id: opened_labelset?.id ? opened_labelset.id : "", ...changedValues}); updateLabelSet({ id: opened_labelset?.id ? opened_labelset.id : "", ...changedValues, @@ -319,9 +356,6 @@ export const LabelSetEditModal = ({ } }; - // console.log("Labels is", labels); - - // Split out the labels by type let text_labels = labels.filter( (label): label is AnnotationLabelType => !!label && label !== undefined && label.labelType === LabelType.TokenLabel @@ -338,18 +372,22 @@ export const LabelSetEditModal = ({ (label): label is AnnotationLabelType => !!label && label.labelType === LabelType.MetadataLabel ); - // console.log("Filtered by type", text_labels, doc_type_labels, relationship_labels); + let span_labels = labels.filter( + (label): label is AnnotationLabelType => + !!label && label.labelType === LabelType.SpanLabel + ); - //Filter the text & doc label sets: let text_label_fuse = new Fuse(text_labels, fuse_options); let doc_label_fuse = new Fuse(doc_type_labels, fuse_options); let relationship_label_fuse = new Fuse(relationship_labels, fuse_options); let metadata_label_fuse = new Fuse(metadata_labels, fuse_options); + let span_label_fuse = new Fuse(span_labels, fuse_options); let text_label_results: AnnotationLabelType[] = []; let doc_label_results: AnnotationLabelType[] = []; let relationship_label_results: AnnotationLabelType[] = []; let metadata_label_results: AnnotationLabelType[] = []; + let span_label_results: AnnotationLabelType[] = []; if (searchTerm.length > 0) { text_label_results = text_label_fuse @@ -364,53 +402,33 @@ export const LabelSetEditModal = ({ metadata_label_results = metadata_label_fuse .search(searchTerm) .map((item) => item.item) as AnnotationLabelType[]; + span_label_results = span_label_fuse + .search(searchTerm) + .map((item) => item.item) as AnnotationLabelType[]; } else { text_label_results = text_labels; doc_label_results = doc_type_labels; relationship_label_results = relationship_labels; metadata_label_results = metadata_labels; + span_label_results = span_labels; } - //Build text label components - let text_data_labels: JSX.Element[] = []; - if (text_label_results && text_label_results.length > 0) { - text_data_labels = text_label_results.map((label, index) => { + const renderLabelCards = (labels: AnnotationLabelType[]) => { + if (labels.length === 0) { return ( - label.id).includes(label.id)} - onDelete={() => handleDeleteLabel([label])} - onSelect={toggleLabelSelect} - onSave={updateLabel} - /> - ); - }); - } - - //Build doc label components - let doc_data_labels: JSX.Element[] = []; - if (doc_label_results && doc_label_results.length > 0) { - doc_data_labels = doc_label_results.map((label, index) => { - return ( - label.id).includes(label.id)} - onDelete={() => handleDeleteLabel([label])} - onSelect={toggleLabelSelect} - onSave={updateLabel} - /> + + + + No matching labels found +

Try adjusting your search or create a new label.

+
+
); - }); - } + } - // Build relationship label components - let relationship_data_labels: JSX.Element[] = []; - if (relationship_label_results && relationship_label_results.length > 0) { - relationship_data_labels = relationship_label_results.map( - (label, index) => { - return ( + return ( + + {labels.map((label, index) => ( - ); - } + ))} + ); - } - - // Build metadata label components - let metadata_data_labels: JSX.Element[] = []; - if (metadata_label_results && metadata_label_results.length > 0) { - metadata_data_labels = metadata_label_results.map((label, index) => { - return ( - label.id).includes(label.id)} - onDelete={() => handleDeleteLabel([label])} - onSelect={toggleLabelSelect} - onSave={updateLabel} - /> - ); - }); - } + }; const panes = [ { @@ -451,15 +452,7 @@ export const LabelSetEditModal = ({ content: "Details", }, render: () => ( - + - + ), }, { menuItem: { key: "metadata", icon: "braille", - content: `Metadata (${metadata_data_labels?.length})`, + content: `Metadata (${metadata_label_results.length})`, }, render: () => ( - - {metadata_data_labels} - + {renderLabelCards(metadata_label_results)} ), }, { menuItem: { key: "text", icon: "language", - content: `Text (${text_data_labels?.length})`, + content: `Text (${text_label_results.length})`, }, - render: () => ( - - {text_data_labels} - - ), + render: () => {renderLabelCards(text_label_results)}, }, { menuItem: { - key: "text", + key: "doc", icon: "file pdf outline", - content: `Doc Types (${doc_data_labels?.length})`, + content: `Doc Types (${doc_label_results.length})`, }, - render: () => ( - - {doc_data_labels} - - ), + render: () => {renderLabelCards(doc_label_results)}, }, { menuItem: { key: "relation", icon: "handshake outline", - content: `Relations (${relationship_data_labels?.length})`, + content: `Relations (${relationship_label_results.length})`, }, render: () => ( - - {relationship_data_labels} - + {renderLabelCards(relationship_label_results)} ), }, + { + menuItem: { + key: "span", + icon: "i cursor", + content: `Spans (${span_label_results.length})`, + }, + render: () => {renderLabelCards(span_label_results)}, + }, ]; let button_actions: DropdownActionProps[] = []; if ( - [1, 2, 3, 4].includes(parseInt(`${activeIndex}`)) && + [1, 2, 3, 4, 5].includes(parseInt(`${activeIndex}`)) && my_permissions.includes(PermissionTypes.CAN_UPDATE) ) { button_actions.push({ @@ -586,7 +543,9 @@ export const LabelSetEditModal = ({ : activeIndex === 3 ? "Create Document Type Label" : activeIndex === 4 - ? "Create OCR Label" + ? "Create Relationship Label" + : activeIndex === 5 + ? "Create Span Label" : "", icon: "plus", action_function: @@ -598,6 +557,8 @@ export const LabelSetEditModal = ({ ? () => handleCreateDocumentLabel() : activeIndex === 4 ? () => handleCreateRelationshipLabel() + : activeIndex === 5 + ? () => handleCreateSpanLabel() : () => {}, }); } @@ -615,101 +576,54 @@ export const LabelSetEditModal = ({ } return ( - toggleModal()} size="large"> - {label_set_loading || create_loading ? ( + toggleModal()}> + {(label_set_loading || create_loading) && ( - ) : ( - <> )} - -
-
- - - Edit Label Set:{" "} - {updatedObject ? (updatedObject as LabelSetType).title : ""} - -
-
-
- - setSearchTerm(value)} - actions={button_actions} - placeholder="Search for label by description or name..." - value={searchTerm} - /> - - - - - +
+ + + Edit Label Set:{" "} + {updatedObject ? (updatedObject as LabelSetType).title : ""} + +
+ + + + setSearchTerm(value)} + actions={button_actions} + placeholder="Search for label by description or name..." + value={searchTerm} /> -
-
+ + + - {canSave ? ( - - ) : ( - <> )} -
+ ); }; diff --git a/frontend/src/components/layout/CardLayout.tsx b/frontend/src/components/layout/CardLayout.tsx index aa3c2243..e59c3bba 100644 --- a/frontend/src/components/layout/CardLayout.tsx +++ b/frontend/src/components/layout/CardLayout.tsx @@ -8,6 +8,7 @@ interface CardLayoutProps { Modals?: React.ReactChild | React.ReactChild[]; BreadCrumbs?: React.ReactChild | null | undefined; SearchBar: React.ReactChild; + style?: React.CSSProperties; } const StyledSegment = styled(Segment)` @@ -54,6 +55,7 @@ export const CardLayout: React.FC = ({ Modals, BreadCrumbs, SearchBar, + style, }) => { const { width } = useWindowDimensions(); const use_mobile = width <= 400; @@ -63,7 +65,7 @@ export const CardLayout: React.FC = ({ {Modals} diff --git a/frontend/src/components/layout/CreateAndSearchBar.tsx b/frontend/src/components/layout/CreateAndSearchBar.tsx index cbfa4ab3..5cc0f8b8 100644 --- a/frontend/src/components/layout/CreateAndSearchBar.tsx +++ b/frontend/src/components/layout/CreateAndSearchBar.tsx @@ -1,7 +1,17 @@ +// Start of Selection import React, { useState } from "react"; -import { Button, Form, Dropdown, Popup } from "semantic-ui-react"; +import { + Button, + Form, + Dropdown, + Popup, + InputOnChangeData, +} from "semantic-ui-react"; import styled from "styled-components"; +/** + * Props for each dropdown action item. + */ export interface DropdownActionProps { icon: string; title: string; @@ -10,6 +20,9 @@ export interface DropdownActionProps { action_function: (args?: any) => any | void; } +/** + * Props for the CreateAndSearchBar component. + */ interface CreateAndSearchBarProps { actions: DropdownActionProps[]; filters?: JSX.Element | JSX.Element[]; @@ -18,6 +31,12 @@ interface CreateAndSearchBarProps { onChange?: (search_string: string) => any | void; } +/** + * CreateAndSearchBar component provides a search input with optional filter and action dropdowns. + * + * @param {CreateAndSearchBarProps} props - The properties passed to the component. + * @returns {JSX.Element} The rendered search bar component. + */ export const CreateAndSearchBar: React.FC = ({ actions, filters, @@ -36,15 +55,24 @@ export const CreateAndSearchBar: React.FC = ({ /> )); + const handleInputChange = ( + e: React.ChangeEvent, + data: InputOnChangeData + ) => { + if (onChange) { + onChange(data.value); + } + }; + return (
- onChange && onChange(value as string)} + onChange={handleInputChange} fluid /> @@ -54,9 +82,10 @@ export const CreateAndSearchBar: React.FC = ({ {filters && ( setIsFilterOpen(!isFilterOpen)} + aria-label="Filter" /> } content={{filters}} @@ -65,43 +94,103 @@ export const CreateAndSearchBar: React.FC = ({ onClose={() => setIsFilterOpen(false)} onOpen={() => setIsFilterOpen(true)} position="bottom right" + pinned /> )} {actions.length > 0 && ( - - }> + + }> {actionItems} - + )}
); }; +/** + * Container for the search bar, removing the blue tint and applying a neutral background. + */ const SearchBarContainer = styled.div` display: flex; align-items: center; justify-content: space-between; padding: 1rem; - background-color: #fff; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); - border-radius: 4px; + background: #ffffff; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 12px; `; +/** + * Wrapper for the search input to control its growth and margin. + */ const SearchInputWrapper = styled.div` flex-grow: 1; margin-right: 1rem; max-width: 50vw; `; +/** + * Styled form input with customized border and focus effects. + */ +const StyledFormInput = styled(Form.Input)` + .ui.input > input { + border-radius: 20px; + border: 1px solid #ccc; + transition: all 0.3s ease; + + &:focus { + box-shadow: 0 0 0 2px #aaa; + } + } +`; + +/** + * Wrapper for action buttons, aligning them with appropriate spacing. + */ const ActionsWrapper = styled.div` display: flex; align-items: center; - gap: 0.5rem; + gap: 0.75rem; +`; + +/** + * Styled button for the filter, ensuring a consistent size and appearance. + */ +const StyledButton = styled(Button)` + border-radius: 20px; + background: #333; + color: white; + transition: background 0.3s ease; + padding: 0.5rem; + + &:hover { + background: #555; + } +`; + +/** + * Styled button group removing unnecessary styling to ensure sane sizing. + */ +const StyledButtonGroup = styled(Button.Group)` + .ui.button { + border-radius: 4px; + padding: 0.5rem 1rem; + background: #28a745; + color: white; + transition: background 0.3s ease; + + &:hover { + background: #218838; + } + } `; +/** + * Content container for the filter popup, ensuring proper anchoring and styling. + */ const FilterPopoverContent = styled.div` max-height: 300px; overflow-y: auto; @@ -109,6 +198,9 @@ const FilterPopoverContent = styled.div` display: flex; flex-direction: column; gap: 1rem; + background: #ffffff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); /* Customize scrollbar */ &::-webkit-scrollbar { @@ -117,6 +209,7 @@ const FilterPopoverContent = styled.div` &::-webkit-scrollbar-track { background: #f1f1f1; + border-radius: 4px; } &::-webkit-scrollbar-thumb { diff --git a/frontend/src/components/queries/CorpusQueryList.tsx b/frontend/src/components/queries/CorpusQueryList.tsx index 32120c4c..4eb4a5f8 100644 --- a/frontend/src/components/queries/CorpusQueryList.tsx +++ b/frontend/src/components/queries/CorpusQueryList.tsx @@ -20,7 +20,7 @@ import { GetCorpusQueriesOutput, GetCorpusQueriesInput, } from "../../graphql/queries"; -import { CorpusQueryType } from "../../graphql/types"; +import { CorpusQueryType } from "../../types/graphql-api"; import { QueryList } from "./QueryList"; export const CorpusQueryList = ({ diff --git a/frontend/src/components/queries/CorpusQueryListItem.tsx b/frontend/src/components/queries/CorpusQueryListItem.tsx index 362dee3d..462fa240 100644 --- a/frontend/src/components/queries/CorpusQueryListItem.tsx +++ b/frontend/src/components/queries/CorpusQueryListItem.tsx @@ -1,5 +1,5 @@ import { Table, Icon, Button } from "semantic-ui-react"; -import { CorpusQueryType, ExportObject } from "../../graphql/types"; +import { CorpusQueryType, ExportObject } from "../../types/graphql-api"; import { DateTimeWidget } from "../widgets/data-display/DateTimeWidget"; import { openedQueryObj } from "../../graphql/cache"; diff --git a/frontend/src/components/queries/QueryList.tsx b/frontend/src/components/queries/QueryList.tsx index 52c50076..3991dacc 100644 --- a/frontend/src/components/queries/QueryList.tsx +++ b/frontend/src/components/queries/QueryList.tsx @@ -1,6 +1,10 @@ import { Table, Dimmer, Loader } from "semantic-ui-react"; import { FetchMoreOnVisible } from "../../components/widgets/infinite_scroll/FetchMoreOnVisible"; -import { CorpusQueryType, ExtractType, PageInfo } from "../../graphql/types"; +import { + CorpusQueryType, + ExtractType, + PageInfo, +} from "../../types/graphql-api"; import { CorpusQueryListItem } from "./CorpusQueryListItem"; interface QueryListProps { diff --git a/frontend/src/components/queries/QueryResultsViewer.tsx b/frontend/src/components/queries/QueryResultsViewer.tsx index c153a5ba..136e39da 100644 --- a/frontend/src/components/queries/QueryResultsViewer.tsx +++ b/frontend/src/components/queries/QueryResultsViewer.tsx @@ -12,9 +12,7 @@ import { Popup, } from "semantic-ui-react"; import ReactMarkdown from "react-markdown"; -import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; -import { vs } from "react-syntax-highlighter/dist/esm/styles/prism"; -import { CorpusQueryType, ServerAnnotationType } from "../../graphql/types"; +import { CorpusQueryType, ServerAnnotationType } from "../../types/graphql-api"; import { displayAnnotationOnAnnotatorLoad, onlyDisplayTheseAnnotations, @@ -22,6 +20,8 @@ import { selectedAnnotation, } from "../../graphql/cache"; import wait_icon from "../../assets/icons/waiting for robo.webp"; +import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/prism-light"; +import { vs } from "react-syntax-highlighter/dist/esm/styles/prism"; interface QueryResultsViewerProps { query_obj: CorpusQueryType; @@ -37,6 +37,7 @@ const QueryResultsViewer: React.FC = ({ useEffect(() => { if (viewSourceAnnotation) { + console.log("viewSourceAnnotation", viewSourceAnnotation); displayAnnotationOnAnnotatorLoad(viewSourceAnnotation); selectedAnnotation(viewSourceAnnotation); openedDocument(viewSourceAnnotation.document); diff --git a/frontend/src/components/types.ts b/frontend/src/components/types.ts index 82678761..899c8378 100644 --- a/frontend/src/components/types.ts +++ b/frontend/src/components/types.ts @@ -1,5 +1,8 @@ import { ReactElement } from "react"; -import { AnnotationLabelType, LabelDisplayBehavior } from "../graphql/types"; +import { + AnnotationLabelType, + LabelDisplayBehavior, +} from "../types/graphql-api"; import { PDFPageInfo } from "./annotator/context"; /** @@ -197,13 +200,18 @@ export type TokenId = { tokenIndex: number; }; +export type SpanAnnotationJson = { + start: number; + end: number; +}; + export type SinglePageAnnotationJson = { bounds: BoundingBox; tokensJsons: TokenId[]; rawText: string; }; -export type TextSearchResult = { +export type TextSearchTokenResult = { id: number; tokens: Record; bounds: Record; @@ -212,7 +220,16 @@ export type TextSearchResult = { end_page: number; }; +export type TextSearchSpanResult = { + id: number; + start_index: number; + end_index: number; + fullContext: ReactElement | null; + text: string; +}; + export type MultipageAnnotationJson = Record; + export interface PageProps { pageInfo: PDFPageInfo; doc_permissions: PermissionTypes[]; diff --git a/frontend/src/components/widgets/CRUD/CRUDModal.tsx b/frontend/src/components/widgets/CRUD/CRUDModal.tsx index 1d20713b..9206f188 100644 --- a/frontend/src/components/widgets/CRUD/CRUDModal.tsx +++ b/frontend/src/components/widgets/CRUD/CRUDModal.tsx @@ -3,8 +3,14 @@ import { Button, Modal, Icon, Header } from "semantic-ui-react"; import _ from "lodash"; import { CRUDWidget } from "./CRUDWidget"; import { CRUDProps, LooseObject, PropertyWidgets } from "../../types"; -import { HorizontallyCenteredDiv } from "../../layout/Wrappers"; +import { + HorizontallyCenteredDiv, + VerticallyCenteredDiv, +} from "../../layout/Wrappers"; +/** + * Props for the ObjectCRUDModal component. + */ export interface ObjectCRUDModalProps extends CRUDProps { open: boolean; oldInstance: Record; @@ -14,6 +20,13 @@ export interface ObjectCRUDModalProps extends CRUDProps { children?: React.ReactNode; } +/** + * CRUDModal component provides a modal interface for creating, viewing, and editing instances. + * It integrates the CRUDWidget for form handling and supports custom property widgets. + * + * @param {ObjectCRUDModalProps} props - The properties passed to the component. + * @returns {JSX.Element} The rendered CRUD modal component. + */ export function CRUDModal({ open, mode, @@ -30,62 +43,73 @@ export function CRUDModal({ onSubmit, onClose, children, -}: ObjectCRUDModalProps) { - const [instance_obj, setInstanceObj] = useState( - oldInstance ? oldInstance : {} +}: ObjectCRUDModalProps): JSX.Element { + const [instanceObj, setInstanceObj] = useState>( + oldInstance || {} ); - const [updated_fields_obj, setUpdatedFields] = useState({ - id: oldInstance?.id ? oldInstance.id : -1, + const [updatedFieldsObj, setUpdatedFields] = useState>({ + id: oldInstance?.id ?? -1, }); - const can_write = mode !== "VIEW" && (mode === "CREATE" || mode === "EDIT"); + const canWrite = mode !== "VIEW" && (mode === "CREATE" || mode === "EDIT"); useEffect(() => { - console.log("CRUD updated fields obj", updated_fields_obj); - }, [updated_fields_obj]); + console.log("CRUD updated fields obj", updatedFieldsObj); + }, [updatedFieldsObj]); useEffect(() => { console.log("oldInstance changed", oldInstance); - setInstanceObj(oldInstance ? oldInstance : {}); - if (oldInstance.length >= 0 && oldInstance.hasOwnProperty("id")) { + setInstanceObj(oldInstance || {}); + if ( + Array.isArray(oldInstance) && + oldInstance.length > 0 && + typeof oldInstance[0] === "object" && + "id" in oldInstance[0] + ) { + setUpdatedFields({ id: oldInstance[0].id }); + } else if ( + typeof oldInstance === "object" && + oldInstance !== null && + "id" in oldInstance + ) { setUpdatedFields({ id: oldInstance.id }); } }, [oldInstance]); - const handleModelChange = (updated_fields: LooseObject) => { - console.log("HandleModelChange: ", updated_fields); - setInstanceObj((instance_obj) => ({ ...instance_obj, ...updated_fields })); - setUpdatedFields((updated_fields_obj) => ({ - ...updated_fields_obj, - ...updated_fields, + /** + * Handles changes in the model and updates the state accordingly. + * + * @param {LooseObject} updatedFields - The updated fields from the form. + */ + const handleModelChange = (updatedFields: LooseObject): void => { + console.log("HandleModelChange: ", updatedFields); + setInstanceObj((prevObj) => ({ ...prevObj, ...updatedFields })); + setUpdatedFields((prevFields) => ({ + ...prevFields, + ...updatedFields, })); }; - let ui_schema_as_applied = { ...uiSchema }; - if (!can_write) { - ui_schema_as_applied["ui:readonly"] = true; - } + const appliedUISchema = useMemo(() => { + return canWrite ? { ...uiSchema } : { ...uiSchema, "ui:readonly": true }; + }, [uiSchema, canWrite]); - let listening_children: JSX.Element[] = []; - - // If we need specific widgets to render and interact with certain fields, loop over the dict between field names and widgets - // and inject listeners and obj values - if (propertyWidgets) { - const keys = Object.keys(propertyWidgets); - - // iterate over object - keys.forEach((key, index) => { - if (React.isValidElement(propertyWidgets[key])) { - listening_children?.push( - React.cloneElement(propertyWidgets[key], { - [key]: instance_obj ? instance_obj[key] : "", + const listeningChildren: JSX.Element[] = useMemo(() => { + if (!propertyWidgets) return []; + return Object.keys(propertyWidgets) + .map((key, index) => { + const widget = propertyWidgets[key]; + if (React.isValidElement(widget)) { + return React.cloneElement(widget, { + [key]: instanceObj[key] || "", onChange: handleModelChange, key: index, - }) - ); - } - }); - } + }); + } + return null; + }) + .filter(Boolean) as JSX.Element[]; + }, [propertyWidgets, instanceObj, handleModelChange]); const descriptiveName = useMemo( () => modelName.charAt(0).toUpperCase() + modelName.slice(1), @@ -95,22 +119,16 @@ export function CRUDModal({ const headerText = useMemo(() => { switch (mode) { case "EDIT": - return `Edit ${descriptiveName}: ${instance_obj.title}`; + return `Edit ${descriptiveName}: ${instanceObj.title}`; case "VIEW": return `View ${descriptiveName}`; default: return `Create ${descriptiveName}`; } - }, [mode, descriptiveName, instance_obj.title]); + }, [mode, descriptiveName, instanceObj.title]); return ( - onClose()} - > +
@@ -127,9 +145,9 @@ export function CRUDModal({ - {listening_children} + {listeningChildren} + {children} - - {can_write && onSubmit && !_.isEqual(oldInstance, instance_obj) ? ( - - ) : ( - <> - )} + {canWrite && onSubmit && !_.isEqual(oldInstance, instanceObj) && ( + + )} + ); diff --git a/frontend/src/components/widgets/CRUD/CRUDWidget.tsx b/frontend/src/components/widgets/CRUD/CRUDWidget.tsx index 458ebe65..f0b37371 100644 --- a/frontend/src/components/widgets/CRUD/CRUDWidget.tsx +++ b/frontend/src/components/widgets/CRUD/CRUDWidget.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo } from "react"; -import { Header, Icon, Segment, Label } from "semantic-ui-react"; +import { Header, Icon, Segment, Label, Grid } from "semantic-ui-react"; import Form from "@rjsf/semantic-ui"; import { HorizontallyCenteredDiv, @@ -8,13 +8,26 @@ import { import { FilePreviewAndUpload } from "../file-controls/FilePreviewAndUpload"; import { CRUDProps, LooseObject } from "../../types"; -interface CRUDWidgetProps extends CRUDProps { - instance: Record; +/** + * Props for the CRUDWidget component. + * + * @template T - The type of the instance being managed. + */ +interface CRUDWidgetProps> extends CRUDProps { + instance: T | Partial; showHeader: boolean; - handleInstanceChange: (a: any) => void; + handleInstanceChange: (updatedInstance: T) => void; } -export const CRUDWidget: React.FC = ({ +/** + * CRUDWidget component provides a form interface for creating, viewing, and editing instances. + * It includes optional file upload functionality and responsive layout adjustments. + * + * @template T - The type of the instance being managed. + * @param {CRUDWidgetProps} props - The properties passed to the component. + * @returns {JSX.Element} The rendered CRUD widget component. + */ +export const CRUDWidget = >({ mode, instance, modelName, @@ -27,24 +40,36 @@ export const CRUDWidget: React.FC = ({ dataSchema, showHeader, handleInstanceChange, -}) => { +}: CRUDWidgetProps): JSX.Element => { const canWrite = mode === "CREATE" || mode === "EDIT"; + /** + * Cleans the form data by retaining only the properties defined in the data schema. + * + * @param {LooseObject} instanceData - The current instance data. + * @param {LooseObject} schema - The data schema defining the properties. + * @returns {Partial} The cleaned form data. + */ const cleanFormData = useCallback( - (instance: LooseObject, dataSchema: LooseObject) => { - return Object.keys(dataSchema.properties).reduce((acc, key) => { - if (key in instance) { - acc[key] = instance[key]; + (instanceData: LooseObject, schema: LooseObject): Partial => { + return Object.keys(schema.properties).reduce((acc, key) => { + if (key in instanceData) { + acc[key as keyof T] = instanceData[key]; } return acc; - }, {} as Record); + }, {} as Partial); }, [] ); + /** + * Handles changes in the form data and propagates them upwards. + * + * @param {Record} param0 - The form data change event. + */ const handleChange = useCallback( ({ formData }: Record) => { - handleInstanceChange(formData); + handleInstanceChange(formData as T); }, [handleInstanceChange] ); @@ -66,7 +91,7 @@ export const CRUDWidget: React.FC = ({ }, [mode, descriptiveName, instance.title]); const formData = useMemo( - () => cleanFormData(instance, dataSchema), + () => cleanFormData(instance as T, dataSchema), [instance, dataSchema, cleanFormData] ); @@ -75,8 +100,13 @@ export const CRUDWidget: React.FC = ({ {showHeader && (
-
- +
+ {headerText} {`Values for: ${descriptiveName}`} @@ -87,31 +117,63 @@ export const CRUDWidget: React.FC = ({ )} - - {hasFile && ( -
- - - handleInstanceChange({ [fileField]: data, filename }) - } - /> -
- )} -
- <> -
+ + + {hasFile && ( + + + + + handleInstanceChange({ + ...instance, + [fileField]: data, + filename, + } as T) + } + /> + + + )} + + +
+ + + + <> + + + <> + + + +
+
+
+
diff --git a/frontend/src/components/widgets/CRUD/LabelSetSelector.tsx b/frontend/src/components/widgets/CRUD/LabelSetSelector.tsx index 9caeaff7..d9f043ae 100644 --- a/frontend/src/components/widgets/CRUD/LabelSetSelector.tsx +++ b/frontend/src/components/widgets/CRUD/LabelSetSelector.tsx @@ -8,7 +8,7 @@ import { GetLabelsetOutputs, GET_LABELSETS, } from "../../../graphql/queries"; -import { LabelSetType } from "../../../graphql/types"; +import { LabelSetType } from "../../../types/graphql-api"; interface LabelSetSelectorProps { read_only?: boolean; diff --git a/frontend/src/components/widgets/ModelFieldBuilder.tsx b/frontend/src/components/widgets/ModelFieldBuilder.tsx new file mode 100644 index 00000000..c762c465 --- /dev/null +++ b/frontend/src/components/widgets/ModelFieldBuilder.tsx @@ -0,0 +1,208 @@ +import React, { useState } from "react"; +import { Button, Form, Grid } from "semantic-ui-react"; +import { motion, AnimatePresence } from "framer-motion"; +import styled from "styled-components"; + +interface FieldType { + fieldName: string; + fieldType: string; + id: string; // Added for stable animations +} + +interface ModelFieldBuilderProps { + onFieldsChange: (fields: FieldType[]) => void; + initialFields?: FieldType[]; +} + +const containerVariants = { + hidden: { + opacity: 0, + transition: { staggerChildren: 0.05, staggerDirection: -1 }, + }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2, + }, + }, +}; + +const fieldVariants = { + hidden: { + opacity: 0, + x: -20, + transition: { type: "tween" }, + }, + visible: { + opacity: 1, + x: 0, + transition: { type: "spring", stiffness: 300, damping: 25 }, + }, + exit: { + opacity: 0, + x: -20, + transition: { duration: 0.2 }, + }, +}; + +const FieldRow = styled(motion.div)` + margin-bottom: 1rem; + background: white; + border-radius: 8px; + padding: 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +`; + +const AddFieldButton = styled(motion.button)` + background: #2185d0; + color: white; + border: none; + border-radius: 20px; + padding: 12px 24px; + cursor: pointer; + width: 100%; + margin-top: 1rem; +`; + +const DeleteButton = styled(Button)` + &.ui.button { + border-radius: 50%; + width: 40px; + height: 40px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + + &:hover { + transform: rotate(90deg); + } + } +`; + +/** + * Converts field definitions to a Pydantic model string representation + * @param fields - Array of field definitions + * @returns Pydantic model as a string + */ +const generatePydanticModel = (fields: FieldType[]): string => { + if (fields.length === 0) return ""; + + const fieldLines = fields + .map((field) => ` ${field.fieldName}: ${field.fieldType}`) + .join("\n"); + + return `class CustomModel(BaseModel):\n${fieldLines}`; +}; + +/** + * Component for building custom model fields with animations. + */ +export const ModelFieldBuilder: React.FC = ({ + onFieldsChange, + initialFields = [], +}) => { + const [fields, setFields] = useState( + initialFields.map((f) => ({ ...f, id: Math.random().toString() })) + ); + + const addField = () => { + const newField = { + fieldName: "", + fieldType: "str", + id: Math.random().toString(), + }; + setFields([...fields, newField]); + }; + + const removeField = (index: number) => { + const newFields = [...fields]; + newFields.splice(index, 1); + setFields(newFields); + onFieldsChange(newFields); + }; + + const updateField = ( + index: number, + key: "fieldName" | "fieldType", + value: string + ) => { + const newFields = [...fields]; + newFields[index][key] = value; + setFields(newFields); + onFieldsChange(newFields); + }; + + return ( + +
+ + {fields.map((field, index) => ( + + + + + + updateField(index, "fieldName", value) + } + required + fluid + label="Field Name" + /> + + + + updateField(index, "fieldType", data.value as string) + } + required + fluid + label="Field Type" + /> + + + removeField(index)} + as={motion.button} + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.95 }} + /> + + + + + ))} + + + + Add Field + +
+
+ ); +}; diff --git a/frontend/src/components/widgets/color-picker/ColorPickerSegment.tsx b/frontend/src/components/widgets/color-picker/ColorPickerSegment.tsx index 89353aa1..882ba4f7 100644 --- a/frontend/src/components/widgets/color-picker/ColorPickerSegment.tsx +++ b/frontend/src/components/widgets/color-picker/ColorPickerSegment.tsx @@ -1,9 +1,9 @@ import { Segment } from "semantic-ui-react"; -import { SliderPicker } from "react-color"; +import { Sketch } from "@uiw/react-color"; interface ColorPickerSegmentProps { color: string; - setColor: (args: any) => void | any; + setColor: (color: { hex: string }) => void; style?: Record; } @@ -14,7 +14,7 @@ export const ColorPickerSegment = ({ }: ColorPickerSegmentProps) => { return ( - + ); }; diff --git a/frontend/src/components/widgets/data-display/CorpusStatus.tsx b/frontend/src/components/widgets/data-display/CorpusStatus.tsx index f2acd17e..2a9d38c6 100644 --- a/frontend/src/components/widgets/data-display/CorpusStatus.tsx +++ b/frontend/src/components/widgets/data-display/CorpusStatus.tsx @@ -1,6 +1,6 @@ import React from "react"; import styled from "styled-components"; -import { CorpusType } from "../../../graphql/types"; +import { CorpusType } from "../../../types/graphql-api"; import { FileText, Users, Database, X } from "lucide-react"; const StatsContainer = styled.div` diff --git a/frontend/src/components/widgets/data-display/LabelSetStatisticWidget.tsx b/frontend/src/components/widgets/data-display/LabelSetStatisticWidget.tsx index 95d0843a..fbc44b5a 100644 --- a/frontend/src/components/widgets/data-display/LabelSetStatisticWidget.tsx +++ b/frontend/src/components/widgets/data-display/LabelSetStatisticWidget.tsx @@ -1,5 +1,5 @@ import { Icon, Popup, Statistic, Header } from "semantic-ui-react"; -import { LabelSetType } from "../../../graphql/types"; +import { LabelSetType } from "../../../types/graphql-api"; export function LabelSetStatistic({ label_set, diff --git a/frontend/src/components/widgets/data-display/TruncatedText.tsx b/frontend/src/components/widgets/data-display/TruncatedText.tsx index 67e0e05f..f0dc38f2 100644 --- a/frontend/src/components/widgets/data-display/TruncatedText.tsx +++ b/frontend/src/components/widgets/data-display/TruncatedText.tsx @@ -1,19 +1,56 @@ +import React from "react"; import { Popup } from "semantic-ui-react"; -export const TruncatedText = ({ - text, - limit, -}: { +interface TruncatedTextProps { text: string; limit: number; +} + +export const TruncatedText: React.FC = ({ + text, + limit, }) => { - if (text.length > limit) { - return ( - {`${text.slice(0, limit).trim()}…`}

} - /> - ); - } - return

{text}

; + const shouldTruncate = text.length > limit; + + const truncatedText = shouldTruncate + ? `${text.slice(0, limit).trim()}…` + : text; + + return shouldTruncate ? ( + + {text} +
+ } + trigger={ +
+ {truncatedText} +
+ } + position="top center" + hoverable + /> + ) : ( +
+ {text} +
+ ); }; diff --git a/frontend/src/components/widgets/file-controls/FilePreviewAndUpload.tsx b/frontend/src/components/widgets/file-controls/FilePreviewAndUpload.tsx index a5fa0f38..f46f70e3 100644 --- a/frontend/src/components/widgets/file-controls/FilePreviewAndUpload.tsx +++ b/frontend/src/components/widgets/file-controls/FilePreviewAndUpload.tsx @@ -92,7 +92,7 @@ export const FilePreviewAndUpload = ({ /> {!isImage ? ( ) : ( <> diff --git a/frontend/src/components/widgets/modals/AddToCorpusModal.tsx b/frontend/src/components/widgets/modals/AddToCorpusModal.tsx index d50ee144..c8550fe3 100644 --- a/frontend/src/components/widgets/modals/AddToCorpusModal.tsx +++ b/frontend/src/components/widgets/modals/AddToCorpusModal.tsx @@ -27,7 +27,7 @@ import { GetCorpusesOutputs, GET_CORPUSES, } from "../../../graphql/queries"; -import { CorpusType, DocumentType } from "../../../graphql/types"; +import { CorpusType, DocumentType } from "../../../types/graphql-api"; import { toast } from "react-toastify"; import { getPermissions } from "../../../utils/transform"; import { PermissionTypes } from "../../types"; diff --git a/frontend/src/components/widgets/modals/CreateColumnModal.tsx b/frontend/src/components/widgets/modals/CreateColumnModal.tsx index 30cfb08b..1588e476 100644 --- a/frontend/src/components/widgets/modals/CreateColumnModal.tsx +++ b/frontend/src/components/widgets/modals/CreateColumnModal.tsx @@ -1,275 +1,174 @@ -import React, { useEffect, useState } from "react"; -import { - Modal, - Form, - Input, - TextArea, - Grid, - Button, - Header, - Checkbox, - Popup, - Icon, -} from "semantic-ui-react"; +import React, { useState, useCallback, useEffect } from "react"; +import { Modal, Form, Grid, Button } from "semantic-ui-react"; +import { BasicConfigSection } from "./sections/BasicConfigSection"; +import { OutputTypeSection } from "./sections/OutputTypeSection"; +import { ExtractionConfigSection } from "./sections/ExtractionConfigSection"; +import { AdvancedOptionsSection } from "./sections/AdvancedOptionsSection"; import { LooseObject } from "../../types"; -import { ColumnType } from "../../../graphql/types"; -import { ExtractTaskDropdown } from "../selectors/ExtractTaskDropdown"; +import styled from "styled-components"; +import { ColumnType } from "../../../types/graphql-api"; interface CreateColumnModalProps { open: boolean; - existing_column?: ColumnType; + existing_column?: ColumnType | null; onClose: () => void; onSubmit: (data: any) => void; } +interface FieldType { + fieldName: string; + fieldType: string; +} + +interface RequiredFields { + query: string; + primitiveType?: string; + taskName: string; + name: string; + agentic: boolean; +} + +const ModalContent = styled(Modal.Content)` + padding: 2rem !important; +`; + +const StyledGrid = styled(Grid)` + margin: 0 !important; +`; + +/** + * Modal component for creating or editing a data extract column. + * + * @param open - Whether the modal is open. + * @param existing_column - An existing column to edit. + * @param onClose - Function to call when closing the modal. + * @param onSubmit - Function to call with the data upon form submission. + */ export const CreateColumnModal: React.FC = ({ open, existing_column, onClose, onSubmit, }) => { - const [objData, setObjData] = useState( - existing_column ? existing_column : {} + const [formData, setFormData] = useState( + existing_column ? { ...existing_column } : {} + ); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Determine if the existing_column.outputType is a primitive type + const isPrimitiveType = ["str", "int", "float", "bool"].includes( + existing_column?.outputType || "" + ); + + const [outputTypeOption, setOutputTypeOption] = useState( + isPrimitiveType ? "primitive" : "custom" ); useEffect(() => { if (existing_column) { - setObjData(existing_column); + setFormData({ ...existing_column }); + const isPrimitiveType = ["str", "int", "float", "bool"].includes( + existing_column.outputType || "" + ); + setOutputTypeOption(isPrimitiveType ? "primitive" : "custom"); } else { - setObjData({}); + setFormData({}); + setOutputTypeOption("primitive"); } }, [existing_column]); - const { - name, - query, - matchText, - outputType, - limitToLabel, - instructions, - agentic, - extractIsList, - mustContainText, - taskName, - } = objData; - - console.log("existing_column", existing_column); + const handleChange = useCallback( + ( + event: React.SyntheticEvent, + data: any, + fieldName: string + ) => { + const value = data.type === "checkbox" ? data.checked : data.value; + setFormData((prev) => ({ ...prev, [fieldName]: value })); + }, + [] + ); - const handleChange = ( - event: React.ChangeEvent, - data: any, - name: string + const handleOutputTypeChange = ( + e: React.FormEvent, + data: any ) => { - setObjData({ ...objData, [name]: data.value }); + setOutputTypeOption(data.value); + // Reset outputType in formData when outputTypeOption changes + setFormData((prev) => ({ + ...prev, + outputType: data.value === "primitive" ? "" : prev.outputType, + })); }; - const handleSubmit = () => { - console.log("Submit data", objData); - onSubmit(objData); + const isFormValid = useCallback((): boolean => { + const requiredFields: RequiredFields = { + query: formData.query || "", + taskName: formData.taskName || "", + name: formData.name || "", + agentic: formData.agentic ?? false, + ...(outputTypeOption === "primitive" + ? { primitiveType: formData.primitiveType } + : {}), + }; + + return Object.entries(requiredFields).every(([key, value]) => { + if (key === "agentic") return typeof value === "boolean"; + return Boolean(value); + }); + }, [formData, outputTypeOption]); + + const handleSubmit = async () => { + setIsSubmitting(true); + try { + await onSubmit(formData); + onClose(); + } catch (error) { + console.error("Error submitting form:", error); + } finally { + setIsSubmitting(false); + } }; return ( - - Create a New Data Extract Column - + + + {existing_column ? "Edit Column" : "Create a New Data Extract Column"} + +
- - - - - - - setObjData({ ...objData, name: value }) - } - fluid - /> - - - - - - - setObjData({ ...objData, outputType: value }) - } - fluid - /> - - - - - - - - { - if (taskName) { - setObjData({ ...objData, taskName }); - } - }} - taskName={taskName} - /> - - - - - -
Query
- -