From f1cc056df1d1f4f53ca22a496dfce94eb07f6681 Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sat, 15 Jun 2024 16:02:30 -0700 Subject: [PATCH] Lots of fixes for small state transitions and styling. Getting ready for beta release of 2.0.0 --- config/graphql/mutations.py | 2 + .../widgets/modals/EditExtractModal.tsx | 68 +++++++++- frontend/src/extracts/ExtractDataGrid.tsx | 93 ------------- frontend/src/extracts/datagrid/DataCell.tsx | 57 ++++---- frontend/src/extracts/datagrid/DataGrid.tsx | 79 ++++++++++-- .../src/extracts/list/ExtractListItem.tsx | 1 - frontend/src/graphql/queries.ts | 14 +- frontend/src/tests/ExtractDataGrid.test.tsx | 122 ------------------ opencontractserver/tasks/extract_tasks.py | 8 +- 9 files changed, 170 insertions(+), 274 deletions(-) delete mode 100644 frontend/src/extracts/ExtractDataGrid.tsx delete mode 100644 frontend/src/tests/ExtractDataGrid.test.tsx diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index 588666f5..fad57fbe 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -195,6 +195,7 @@ def mutate(root, info, datacell_id): pk = from_global_id(datacell_id)[1] obj = Datacell.objects.get(pk=pk) obj.approved_by = info.context.user + obj.rejected_by = None obj.save() message = "SUCCESS!" @@ -226,6 +227,7 @@ def mutate(root, info, datacell_id): pk = from_global_id(datacell_id)[1] obj = Datacell.objects.get(pk=pk) obj.rejected_by = info.context.user + obj.approved_by = None obj.save() message = "SUCCESS!" diff --git a/frontend/src/components/widgets/modals/EditExtractModal.tsx b/frontend/src/components/widgets/modals/EditExtractModal.tsx index 583cb50b..41cc1cb9 100644 --- a/frontend/src/components/widgets/modals/EditExtractModal.tsx +++ b/frontend/src/components/widgets/modals/EditExtractModal.tsx @@ -5,6 +5,8 @@ import { Button, Modal, Icon, + Dimmer, + Loader, } from "semantic-ui-react"; import { ColumnType, @@ -45,9 +47,7 @@ import { CreateColumnModal } from "./CreateColumnModal"; import { addingColumnToExtract, editingColumnForExtract, - showEditExtractModal, } from "../../../graphql/cache"; -import { EditColumnModal } from "./EditColumnModal"; interface EditExtractModalProps { ext: ExtractType | null; @@ -163,7 +163,7 @@ export const EditExtractModal = ({ }); }; - const [createColumn] = useMutation< + const [createColumn, { loading: create_column_loading }] = useMutation< RequestCreateColumnOutputType, RequestCreateColumnInputType >(REQUEST_CREATE_COLUMN, { @@ -192,6 +192,8 @@ export const EditExtractModal = ({ error, data: extract_data, refetch, + startPolling, + stopPolling, } = useQuery( REQUEST_GET_EXTRACT, { @@ -202,7 +204,7 @@ export const EditExtractModal = ({ } ); - const [updateColumn] = useMutation< + const [updateColumn, { loading: update_column_loading }] = useMutation< RequestUpdateColumnOutputType, RequestUpdateColumnInputType >(REQUEST_UPDATE_COLUMN, { @@ -235,21 +237,55 @@ export const EditExtractModal = ({ }, }); - const [startExtract] = useMutation< + const [startExtract, { loading: start_extract_loading }] = useMutation< RequestStartExtractOutputType, RequestStartExtractInputType >(REQUEST_START_EXTRACT, { onCompleted: (data) => { toast.success("SUCCESS! Started extract."); - setExtract((e) => { - return { ...e, ...data.startExtract.obj }; + setExtract((old_extract) => { + return { ...old_extract, ...data.startExtract.obj }; }); + startPolling(30000); }, onError: (err) => { toast.error("ERROR! Could not start extract."); + stopPolling(); }, }); + // Setup long-polling (or stop it) for incomplete jobs that are running + useEffect(() => { + if (extract_data && extract_data.extract.started) { + // Start polling every 30 seconds + startPolling(30000); + + // Set up a timeout to stop polling after 10 minutes + const timeoutId = setTimeout(() => { + stopPolling(); + toast.info( + "Job is taking too long... polling paused after 10 minutes." + ); + }, 600000); + + // Clean up the timeout when the component unmounts or the extract changes + return () => { + clearTimeout(timeoutId); + }; + } + }, [extract_data, startPolling, stopPolling]); + + useEffect(() => { + if ( + extract_data && + (extract_data.extract.stacktrace !== null || + extract_data.extract.finished !== null) + ) { + // Stop polling when the extract has failed or finished + stopPolling(); + } + }, [extract_data, stopPolling]); + useEffect(() => { if (extract) { refetch(); @@ -307,6 +343,23 @@ export const EditExtractModal = ({ justifyContent: "center", }} > + {start_extract_loading ? ( + + Running... + + ) : ( + <> + )} + {loading || + update_column_loading || + add_docs_loading || + remove_docs_loading ? ( + + Loading... + + ) : ( + <> + )} Editing Extract {extract.name}
refetch({ id: extract.id })} extract={extract} cells={cells} rows={rows} diff --git a/frontend/src/extracts/ExtractDataGrid.tsx b/frontend/src/extracts/ExtractDataGrid.tsx deleted file mode 100644 index 7759c1c5..00000000 --- a/frontend/src/extracts/ExtractDataGrid.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from "react"; -import { Button, Table } from "semantic-ui-react"; -import { useMutation, useQuery } from "@apollo/client"; -import { toast } from "react-toastify"; - -import { ColumnDetails } from "./ColumnDetails"; -import { ExtractType } from "../graphql/types"; -import { REQUEST_GET_EXTRACT } from "../graphql/queries"; -import { REQUEST_CREATE_EXTRACT } from "../graphql/mutations"; - -interface ExtractDataGridProps { - extractId: string; -} - -export const ExtractDataGrid: React.FC = ({ - extractId, -}) => { - const { data, refetch } = useQuery<{ extract: ExtractType }>( - REQUEST_GET_EXTRACT, - { - variables: { id: extractId }, - } - ); - const [startExtract] = useMutation(REQUEST_CREATE_EXTRACT); - - const handleStartExtract = async () => { - try { - await startExtract({ variables: { id: extractId } }); - refetch(); - toast.success("Extract started successfully"); - } catch (error) { - toast.error("Error starting extract"); - } - }; - - if (!data?.extract) { - return null; - } - - const { extract } = data; - const columns = extract.fieldset.columns; - const datacells = extract.extractedDatacells; - const documents = extract.documents ? extract.documents : []; - - console.log("Extract:", extract); - - return ( -
- - - - {columns.edges.map((columnEdge) => ( - - {columnEdge.node.query} - - ))} - - - - {datacells?.edges ? ( - datacells.edges.map((row) => ( - - {columns.edges.map((columnEdge) => ( - - {row && row.node ? row.node.data[columnEdge.node.id] : ""} - - ))} - - )) - ) : ( - <> - )} - -
- {!extract.started && ( - <> -

Edit Columns

- {columns.edges.map((columnEdge) => ( - - ))} - - - )} -
- ); -}; diff --git a/frontend/src/extracts/datagrid/DataCell.tsx b/frontend/src/extracts/datagrid/DataCell.tsx index dd8a70d1..f656351f 100644 --- a/frontend/src/extracts/datagrid/DataCell.tsx +++ b/frontend/src/extracts/datagrid/DataCell.tsx @@ -18,6 +18,7 @@ import { selectedAnnotation, } from "../../graphql/cache"; import { Server } from "http"; +import _ from "lodash"; interface ExtractDatacellProps { cellData: DatacellType; @@ -62,8 +63,7 @@ export const ExtractDatacell = ({ onReject, onEdit, }: ExtractDatacellProps): JSX.Element => { - console.log("Celldata", cellData); - + const [color, setColor] = useState("gray"); const [modalOpen, setModalOpen] = useState(false); const [editData, setEditData] = useState | null>(null); const [viewSourceAnnotations, setViewSourceAnnotations] = useState< @@ -85,43 +85,36 @@ export const ExtractDatacell = ({ } }, [viewSourceAnnotations]); - const viewOnly = - readOnly || (cellData.started && !(cellData.failed || cellData.completed)); - useEffect(() => { setEditData(cellData.correctedData ?? cellData.data ?? {}); }, [cellData]); - const handleEditClick = () => { - setModalOpen(true); - }; - - const handleSave = () => { - if (editData && onEdit) { - onEdit(cellData.id, editData); - } - setModalOpen(false); - }; - const handleCancel = () => { setEditData(null); setModalOpen(false); }; - let color = "light gray"; - if (cellData.failed) { - color = "red"; - } else if (cellData.started && cellData.completed) { - if (cellData.correctedData) { - color = "yellow"; - } else if (cellData.rejectedBy) { - color = "light red"; - } else if (cellData.approvedBy) { - color = "green"; - } else { - color = "light green"; + useEffect(() => { + console.log("Calculate color on new cellData!"); + let calculated_color = "light gray"; + if (cellData.failed) { + calculated_color = "red"; + } else if (cellData.started && cellData.completed) { + console.log("Try to determine "); + if ( + cellData.correctedData !== "{}" && + !_.isEmpty(cellData.correctedData) + ) { + calculated_color = "yellow"; + } else if (cellData.rejectedBy) { + calculated_color = "red"; + } else if (cellData.approvedBy) { + calculated_color = "green"; + } } - } + console.log("Calculated color", cellData, calculated_color); + setColor(calculated_color); + }, [cellData]); const renderJsonPreview = (data: Record) => { const jsonString = JSON.stringify(data, null, 2); @@ -135,13 +128,9 @@ export const ExtractDatacell = ({ ); }; - const handleJsonChange = (newData: Record) => { - setEditData(newData); - }; - return ( <> - + {cellData.started && !cellData.completed ? ( diff --git a/frontend/src/extracts/datagrid/DataGrid.tsx b/frontend/src/extracts/datagrid/DataGrid.tsx index 8a8ba4f0..40b6c0e5 100644 --- a/frontend/src/extracts/datagrid/DataGrid.tsx +++ b/frontend/src/extracts/datagrid/DataGrid.tsx @@ -1,5 +1,13 @@ -import React, { useState } from "react"; -import { Table, Button, Icon, Dropdown, Segment } from "semantic-ui-react"; +import React, { useEffect, useState } from "react"; +import { + Table, + Button, + Icon, + Dropdown, + Segment, + Dimmer, + Loader, +} from "semantic-ui-react"; import { ColumnType, DatacellType, @@ -25,6 +33,7 @@ import { RequestRejectDatacellInputType, RequestRejectDatacellOutputType, } from "../../graphql/mutations"; +import { toast } from "react-toastify"; interface DataGridProps { extract: ExtractType; @@ -33,6 +42,7 @@ interface DataGridProps { onAddDocIds: (extractId: string, documentIds: string[]) => void; onRemoveDocIds: (extractId: string, documentIds: string[]) => void; onRemoveColumnId: (columnId: string) => void; + refetch: () => any; columns: ColumnType[]; } @@ -41,28 +51,50 @@ export const DataGrid = ({ cells, rows, columns, + refetch, onAddDocIds, onRemoveDocIds, onRemoveColumnId, }: DataGridProps) => { - console.log("Datagrid cells: ", cells); - + const [lastCells, setLastCells] = useState(cells); const [isAddingColumn, setIsAddingColumn] = useState(false); const [showAddRowButton, setShowAddRowButton] = useState(false); const [openAddRowModal, setOpenAddRowModal] = useState(false); - const [requestApprove] = useMutation< + useEffect(() => { + setLastCells(cells); + }, [cells]); + + const [requestApprove, { loading: trying_approve }] = useMutation< RequestApproveDatacellOutputType, RequestApproveDatacellInputType - >(REQUEST_APPROVE_DATACELL); - const [requestReject] = useMutation< + >(REQUEST_APPROVE_DATACELL, { + onCompleted: () => { + toast.success("Approved!"); + refetch(); + }, + onError: () => toast.error("Could not register feedback!"), + }); + const [requestReject, { loading: trying_reject }] = useMutation< RequestRejectDatacellOutputType, RequestRejectDatacellInputType - >(REQUEST_REJECT_DATACELL); - const [requestEdit] = useMutation< + >(REQUEST_REJECT_DATACELL, { + onCompleted: () => { + toast.success("Rejected!"); + refetch(); + }, + onError: () => toast.error("Could not register feedback!"), + }); + const [requestEdit, { loading: trying_edit }] = useMutation< RequestEditDatacellOutputType, RequestEditDatacellInputType - >(REQUEST_EDIT_DATACELL); + >(REQUEST_EDIT_DATACELL, { + onCompleted: () => { + toast.success("Edit Saved!"); + refetch(); + }, + onError: () => toast.error("Could not register feedback!"), + }); return ( setShowAddRowButton(true)} onMouseLeave={() => setShowAddRowButton(false)} > + {trying_approve ? ( + + Approving... + + ) : ( + <> + )} + {trying_edit ? ( + + Editing... + + ) : ( + <> + )} + {trying_reject ? ( + + Rejecting... + + ) : ( + <> + )} + @@ -139,7 +193,7 @@ export const DataGrid = ({ ) : ( rows.map((row) => ( - + {row.title} {!extract.started ? ( @@ -162,7 +216,7 @@ export const DataGrid = ({ )} {columns.map((column) => { - const cell = cells.find( + const cell = lastCells.find( (cell) => cell.document.id === row.id && cell.column.id === column.id @@ -170,6 +224,7 @@ export const DataGrid = ({ if (cell) { return ( requestApprove({ diff --git a/frontend/src/extracts/list/ExtractListItem.tsx b/frontend/src/extracts/list/ExtractListItem.tsx index c4e505c4..85ecacdc 100644 --- a/frontend/src/extracts/list/ExtractListItem.tsx +++ b/frontend/src/extracts/list/ExtractListItem.tsx @@ -74,7 +74,6 @@ export function ExtractItemRow({ color="red" onClick={onDelete} /> -
diff --git a/frontend/src/graphql/queries.ts b/frontend/src/graphql/queries.ts index 058cfbf0..71f0ff86 100644 --- a/frontend/src/graphql/queries.ts +++ b/frontend/src/graphql/queries.ts @@ -844,7 +844,7 @@ export interface RequestAnnotatorDataForDocumentInCorpusOutputs { export const REQUEST_ANNOTATOR_DATA_FOR_DOCUMENT_IN_CORPUS = gql` query ( $selectedDocumentId: ID! - $selectedCorpusId: ID + $selectedCorpusId: ID! $forAnalysisIds: String $preloadAnnotations: Boolean! ) { @@ -1207,6 +1207,8 @@ export const GET_EXPORT = gql` fieldset { fullColumnList { id + instructions + extractIsList limitToLabel languageModel { id @@ -1283,6 +1285,7 @@ export const REQUEST_GET_FIELDSETS = gql` outputType limitToLabel instructions + extractIsList languageModel { id model @@ -1318,6 +1321,7 @@ export const GET_FIELDSET = gql` outputType limitToLabel instructions + extractIsList languageModel { id model @@ -1352,6 +1356,7 @@ export const REQUEST_GET_EXTRACT = gql` id name query + instructions matchText limitToLabel agentic @@ -1430,7 +1435,14 @@ export const REQUEST_GET_EXTRACT = gql` started completed failed + correctedData stacktrace + rejectedBy { + email + } + approvedBy { + email + } } } } diff --git a/frontend/src/tests/ExtractDataGrid.test.tsx b/frontend/src/tests/ExtractDataGrid.test.tsx deleted file mode 100644 index 3b745ec7..00000000 --- a/frontend/src/tests/ExtractDataGrid.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import { MockedProvider } from "@apollo/client/testing"; -import { REQUEST_GET_EXTRACT } from "../graphql/queries"; -import { ExtractDataGrid } from "../extracts/ExtractDataGrid"; -import { generateMockLanguageModel } from "./utils/factories"; - -const mockExtractId = "1"; - -const mockExtract = { - id: "1", - corpus: { - id: "1", - title: "Test Corpus", - }, - name: "Test Extract", - fieldset: { - id: "1", - name: "Test Fieldset", - columns: [ - { - id: "1", - query: "Test Query 1", - languageModel: { - id: "12324", - model: "GPT4", - }, - }, - { - id: "2", - query: "Test Query 2", - languageModel: { - id: "12324", - model: "GPT4", - }, - }, - ], - }, - owner: { - id: "1", - username: "testuser", - }, - created: "2023-06-12T10:00:00", - started: null, - finished: null, - rows: [ - { - id: "1", - data: { - data: "Test Data 1", - }, - dataDefinition: "str", - stacktrace: "", - failed: null, - finished: null, - completed: null, - started: null, - column: { - id: "1", - languageModel: { - id: "12312", - }, - }, - }, - ], -}; - -const mockGetExtractQuery = { - request: { - query: REQUEST_GET_EXTRACT, - variables: { id: mockExtractId }, - }, - result: { - data: { - extract: { - id: mockExtractId, - }, - }, - }, -}; - -describe("ExtractDataGrid", () => { - it("renders the data grid with correct data", async () => { - render( - - - - ); - - expect(await screen.findByText("Test Query 1")).toBeInTheDocument(); - expect(screen.getByText("Test Query 2")).toBeInTheDocument(); - expect(screen.getByText("Test Data 1")).toBeInTheDocument(); - expect(screen.getByText("Test Data 2")).toBeInTheDocument(); - }); - - it("renders the start extract button when the extract has not started", async () => { - const mockExtractNotStarted = { - ...mockExtract, - started: null, - }; - - const mockGetExtractQueryNotStarted = { - request: { - query: REQUEST_GET_EXTRACT, - variables: { id: mockExtractId }, - }, - result: { - data: { - extract: mockExtractNotStarted, - }, - }, - }; - - render( - - - - ); - - expect(await screen.findByText("Start Extract")).toBeInTheDocument(); - }); -}); diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py index 7b119b77..c4159738 100644 --- a/opencontractserver/tasks/extract_tasks.py +++ b/opencontractserver/tasks/extract_tasks.py @@ -50,10 +50,10 @@ def run_extract(extract_id, user_id): extract.started = timezone.now() extract.save() - corpus = extract.corpus fieldset = extract.fieldset - document_ids = corpus.documents.all().values_list("id", flat=True) + document_ids = extract.documents.all().values_list("id", flat=True) + print(f"Run extract {extract_id} over document ids {document_ids}") tasks = [] for document_id in document_ids: @@ -113,8 +113,8 @@ def llama_index_doc_query(cell_id, similarity_top_k=3): results = retriever.retrieve(search_text if search_text else query) for r in results: - print(f"Result {r.node.extra_info['label_id']}:\n{r}") - retrieved_annotation_ids = [n.node.extra_info['label_id'] for n in results] + print(f"Result: {r.node.extra_info}:\n{r}") + retrieved_annotation_ids = [n.node.extra_info['annotation_id'] for n in results] print(f"retrieved_annotation_ids: {retrieved_annotation_ids}") datacell.sources.add(*retrieved_annotation_ids)