From c412222ca5a07a5bb8eb920714325bb314482aa4 Mon Sep 17 00:00:00 2001 From: Subina Date: Thu, 2 Nov 2023 16:27:53 +0545 Subject: [PATCH 01/13] Remove assisted tagging switch --- .../LeftPane/SimplifiedTextView/index.tsx | 1 - .../LeftPane/SimplifiedTextView/styles.css | 7 -- app/views/EntryEdit/LeftPane/index.tsx | 65 ++++++------------- 3 files changed, 21 insertions(+), 52 deletions(-) diff --git a/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx b/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx index 1acdfb8796..4c644cd819 100644 --- a/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx +++ b/app/views/EntryEdit/LeftPane/SimplifiedTextView/index.tsx @@ -326,7 +326,6 @@ function SimplifiedTextView(props: Props) { styles.simplifiedTextView, className, disableAddButton && styles.disabled, - assistedTaggingEnabled && styles.assistedEnabled, )} > {children} diff --git a/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css b/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css index 8d293e7518..5b04b7b114 100644 --- a/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css +++ b/app/views/EntryEdit/LeftPane/SimplifiedTextView/styles.css @@ -16,13 +16,6 @@ margin: var(--dui-spacing-medium) 0; } - &.assisted-enabled { - >*::selection { - background: var(--dui-color-nlp); - color: var(--dui-color-text-on-accent); - } - } - .actions-popup { display: flex; position: absolute; diff --git a/app/views/EntryEdit/LeftPane/index.tsx b/app/views/EntryEdit/LeftPane/index.tsx index 7df23619e9..e43f9a6978 100644 --- a/app/views/EntryEdit/LeftPane/index.tsx +++ b/app/views/EntryEdit/LeftPane/index.tsx @@ -24,7 +24,6 @@ import { QuickActionLink, PendingMessage, Message, - Switch, } from '@the-deep/deep-ui'; import { IoAdd, @@ -37,7 +36,6 @@ import { IoCheckmark, } from 'react-icons/io5'; -import useLocalStorage from '#hooks/useLocalStorage'; import { GeoArea } from '#components/GeoMultiSelectInput'; import LeadPreview from '#components/lead/LeadPreview'; import Screenshot from '#components/Screenshot'; @@ -149,11 +147,6 @@ function LeftPane(props: Props) { : defaultTab, ); - const [ - assistedTaggingEnabled, - onAssistedTaggingStatusChange, - ] = useLocalStorage('assisted-tagging-enabled', false); - useEffect(() => { if (activeTabRef) { activeTabRef.current = { @@ -501,8 +494,7 @@ function LeftPane(props: Props) { ); - const assistedTaggingShown = assistedTaggingEnabled - && !project?.isPrivate + const assistedTaggingShown = !project?.isPrivate && isAssistedTaggingAccessible && frameworkDetails?.assistedTaggingEnabled && (frameworkDetails?.predictionTagsMapping?.length ?? 0) > 0; @@ -545,41 +537,26 @@ function LeftPane(props: Props) { retainMount="lazy" > {(leadPreview?.textExtract?.length ?? 0) > 0 ? ( - <> - {(frameworkDetails?.predictionTagsMapping?.length ?? 0) > 0 - && !project?.isPrivate - && frameworkDetails?.assistedTaggingEnabled - && isAssistedTaggingAccessible - && ( - - )} - - + ) : ( Date: Fri, 24 Nov 2023 14:54:34 +0545 Subject: [PATCH 02/13] Add tags in framework selection --- .../FrameworkTagSelectInput/index.tsx | 145 ++++++++++++++++++ .../FrameworkTagSelectInput/styles.css | 5 + app/views/ProjectEdit/Framework/index.tsx | 27 ++++ package.json | 2 +- yarn.lock | 8 +- 5 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 app/components/selections/FrameworkTagSelectInput/index.tsx create mode 100644 app/components/selections/FrameworkTagSelectInput/styles.css diff --git a/app/components/selections/FrameworkTagSelectInput/index.tsx b/app/components/selections/FrameworkTagSelectInput/index.tsx new file mode 100644 index 0000000000..5159a095ba --- /dev/null +++ b/app/components/selections/FrameworkTagSelectInput/index.tsx @@ -0,0 +1,145 @@ +import React, { useMemo, useCallback } from 'react'; +import { + Button, + BadgeInput, + BadgeInputProps, +} from '@the-deep/deep-ui'; +import { useQuery, gql } from '@apollo/client'; + +import { + FrameworkTagOptionsQuery, + FrameworkTagOptionsQueryVariables, +} from '#generated/types'; + +import styles from './styles.css'; + +const FRAMEWORK_TAGS = gql` + query FrameworkTagOptions( + $page: Int, + $pageSize: Int, + ) { + analysisFrameworkTags( + page: $page, + pageSize: $pageSize, + ) { + page + pageSize + results { + description + id + title + icon { + name + url + } + } + totalCount + } + } +`; + +const PAGE_SIZE = 10; + +type BasicFrameworkTag = NonNullable['analysisFrameworkTags']>['results']>[number]; +const keySelector = (item: BasicFrameworkTag) => item.id; +const labelSelector = (item: BasicFrameworkTag) => item.title; +const titleSelector = (item: BasicFrameworkTag) => item.description; +function iconSelector(item: BasicFrameworkTag) { + if (!item.icon?.url) { + return undefined; + } + return ( + {item.icon.url} + ); +} + +type Props = Omit< + BadgeInputProps, + 'options' | 'keySelector' | 'labelSelector' +>; + +function FrameworkTagSelectInput( + props: Props, +) { + const variables = useMemo(() => ({ + page: 1, + pageSize: PAGE_SIZE, + }), []); + + const { + data, + fetchMore, + } = useQuery( + FRAMEWORK_TAGS, + { + variables, + }, + ); + + const handleShowMoreClick = useCallback(() => { + fetchMore({ + variables: { + ...variables, + page: (data?.analysisFrameworkTags?.page ?? 1) + 1, + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + if (!previousResult.analysisFrameworkTags) { + return previousResult; + } + + const oldFrameworkTags = previousResult.analysisFrameworkTags; + const newFrameworkTags = fetchMoreResult?.analysisFrameworkTags; + + if (!newFrameworkTags) { + return previousResult; + } + + return ({ + ...previousResult, + analysisFrameworkTags: { + ...newFrameworkTags, + results: [ + ...(oldFrameworkTags.results ?? []), + ...(newFrameworkTags.results ?? []), + ], + }, + }); + }, + }); + }, [ + data?.analysisFrameworkTags?.page, + fetchMore, + variables, + ]); + + return ( + <> + + {(data?.analysisFrameworkTags?.totalCount ?? 0) + > (data?.analysisFrameworkTags?.results ?? []).length && ( + + )} + + ); +} + +export default FrameworkTagSelectInput; diff --git a/app/components/selections/FrameworkTagSelectInput/styles.css b/app/components/selections/FrameworkTagSelectInput/styles.css new file mode 100644 index 0000000000..965ca45ff6 --- /dev/null +++ b/app/components/selections/FrameworkTagSelectInput/styles.css @@ -0,0 +1,5 @@ +.icon { + width: auto; + height: 1rem; + object-fit: contain; +} diff --git a/app/views/ProjectEdit/Framework/index.tsx b/app/views/ProjectEdit/Framework/index.tsx index 46be87e2e2..6b06d35f49 100644 --- a/app/views/ProjectEdit/Framework/index.tsx +++ b/app/views/ProjectEdit/Framework/index.tsx @@ -8,6 +8,7 @@ import { import { Button, Container, + Checkbox, Kraken, ListView, Message, @@ -29,6 +30,7 @@ import { import SmartButtonLikeLink from '#base/components/SmartButtonLikeLink'; import useDebouncedValue from '#hooks/useDebouncedValue'; import ProjectContext from '#base/context/ProjectContext'; +import FrameworkTagSelectInput from '#components/selections/FrameworkTagSelectInput'; import { isFiltered } from '#utils/common'; import routes from '#base/configs/routes'; import _ts from '#ts'; @@ -49,7 +51,9 @@ const frameworkKeySelector = (d: FrameworkType) => d.id; export const PROJECT_FRAMEWORKS = gql` query ProjectAnalysisFrameworks( $isCurrentUserMember: Boolean, + $tags: [ID!], $search: String, + $recentlyUsed: Boolean, $page: Int, $pageSize: Int, $createdBy: [ID!], @@ -57,8 +61,10 @@ export const PROJECT_FRAMEWORKS = gql` analysisFrameworks( search: $search, isCurrentUserMember: $isCurrentUserMember + recentlyUsed: $recentlyUsed, page: $page, pageSize: $pageSize, + tags: $tags, createdBy: $createdBy, ) { results { @@ -133,7 +139,9 @@ const relatedToMeLabelSelector = (d: Option) => d.label; type FormType = { relatedToMe?: 'true' | 'false'; + recentlyUsed: boolean; search: string; + tag?: string; }; type FormSchema = ObjectSchema>; @@ -142,12 +150,16 @@ type FormSchemaFields = ReturnType const schema: FormSchema = { fields: (): FormSchemaFields => ({ search: [], + tag: [], + recentlyUsed: [], relatedToMe: [requiredCondition], }), }; const defaultFormValue: PartialForm = { relatedToMe: 'true', + recentlyUsed: false, + tag: undefined, search: '', }; @@ -183,12 +195,16 @@ function ProjectFramework(props: Props) { const analysisFrameworkVariables = useMemo(() => ( { isCurrentUserMember: delayedValue.relatedToMe === 'true' ? true : undefined, + tags: delayedValue.tag ? [delayedValue.tag] : undefined, + recentlyUsed: delayedValue.recentlyUsed, search: delayedValue.search, page: 1, pageSize: PAGE_SIZE, } ), [ delayedValue.relatedToMe, + delayedValue.recentlyUsed, + delayedValue.tag, delayedValue.search, ]); @@ -287,6 +303,17 @@ function ProjectFramework(props: Props) { value={value.search} placeholder={_ts('projectEdit', 'searchLabel')} /> + +
Date: Wed, 22 Nov 2023 13:51:22 +0545 Subject: [PATCH 03/13] Add auto extracted draft entries --- .../LeftPane/AutoEntriesModal/index.tsx | 77 +++++++ app/views/EntryEdit/LeftPane/index.tsx | 208 +++++++++++++----- app/views/EntryEdit/LeftPane/styles.css | 2 +- 3 files changed, 229 insertions(+), 58 deletions(-) create mode 100644 app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx new file mode 100644 index 0000000000..d801b13109 --- /dev/null +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -0,0 +1,77 @@ +import React, { useMemo } from 'react'; +import { isNotDefined } from '@togglecorp/fujs'; +import { gql, useQuery } from '@apollo/client'; +import { + Modal, +} from '@the-deep/deep-ui'; + +import { + AutoEntriesForLeadQuery, + AutoEntriesForLeadQueryVariables, +} from '#generated/types'; + +const AUTO_ENTRIES_FOR_LEAD = gql` + query AutoEntriesForLead( + $projectId: ID!, + $leadIds: [ID!], + ) { + project(id: $projectId) { + assistedTagging { + draftEntryByLeads( + filter: { + draftEntryType: AUTO, + lead: $leadIds, + }) { + id + excerpt + predictionReceivedAt + predictionStatus + } + } + } + } +`; + +interface Props { + onModalClose: () => void; + projectId: string; + leadId: string; +} + +function AutoEntriesModal(props: Props) { + const { + onModalClose, + projectId, + leadId, + } = props; + + const autoEntriesVariables = useMemo(() => ({ + projectId, + leadIds: [leadId], + }), [ + projectId, + leadId, + ]); + + const { + data: autoEntries, + } = useQuery( + AUTO_ENTRIES_FOR_LEAD, + { + skip: isNotDefined(autoEntriesVariables), + variables: autoEntriesVariables, + }, + ); + + console.log('here', autoEntries); + + return ( + + Auto entries here + + ); +} + +export default AutoEntriesModal; diff --git a/app/views/EntryEdit/LeftPane/index.tsx b/app/views/EntryEdit/LeftPane/index.tsx index e43f9a6978..02e15000ab 100644 --- a/app/views/EntryEdit/LeftPane/index.tsx +++ b/app/views/EntryEdit/LeftPane/index.tsx @@ -5,7 +5,7 @@ import { isDefined, randomString, } from '@togglecorp/fujs'; -import { gql, useQuery } from '@apollo/client'; +import { gql, useQuery, useMutation } from '@apollo/client'; import { Tabs, Container, @@ -15,6 +15,7 @@ import { TextInput, QuickActionButton, useBooleanState, + useModalState, Button, QuickActionDropdownMenu, QuickActionDropdownMenuProps, @@ -44,6 +45,8 @@ import { UserContext } from '#base/context/UserContext'; import { LeadPreviewForTextQuery, LeadPreviewForTextQueryVariables, + CreateAutoDraftEntriesMutation, + CreateAutoDraftEntriesMutationVariables, } from '#generated/types'; import { PartialEntryType as EntryInput } from '#components/entry/schema'; @@ -53,6 +56,7 @@ import CanvasDrawModal from './CanvasDrawModal'; import { Lead, EntryImagesMap } from '../index'; import SimplifiedTextView from './SimplifiedTextView'; import EntryItem, { ExcerptModal } from './EntryItem'; +import AutoEntriesModal from './AutoEntriesModal'; import styles from './styles.css'; const LEAD_PREVIEW = gql` @@ -73,6 +77,22 @@ const LEAD_PREVIEW = gql` } `; +const CREATE_AUTO_DRAFT_ENTRIES = gql` + mutation CreateAutoDraftEntries ( + $projectId: ID!, + $leadId: ID!, + ) { + project(id: $projectId) { + assistedTagging { + autoDraftEntryCreate(data: {lead: $leadId}) { + ok + errors + } + } + } + } +`; + const entryKeySelector = (e: EntryInput) => e.clientId; export type TabOptions = 'simplified' | 'original' | 'entries' | undefined; @@ -147,6 +167,12 @@ function LeftPane(props: Props) { : defaultTab, ); + const [ + autoEntriesModalShown, + showAutoEntriesModal, + hideAutoEntriesModal, + ] = useModalState(false); + useEffect(() => { if (activeTabRef) { activeTabRef.current = { @@ -193,6 +219,57 @@ function LeftPane(props: Props) { }, ); + const [ + triggerAutoEntriesCreate, + ] = useMutation( + CREATE_AUTO_DRAFT_ENTRIES, + { + onCompleted: (response) => { + const autoEntriesResponse = response?.project?.assistedTagging + ?.autoDraftEntryCreate; + if (autoEntriesResponse?.ok) { + alert.show( + 'The entries extraction has started.', + { variant: 'success' }, + ); + showAutoEntriesModal(); + } else { + alert.show( + 'Failed to extract entries using NLP.', + { + variant: 'error', + }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to extract entries using NLP.', + { + variant: 'error', + }, + ); + }, + }, + ); + + const handleAutoExtractClick = useCallback(() => { + if (isNotDefined(projectId)) { + return; + } + + triggerAutoEntriesCreate({ + variables: { + projectId, + leadId, + }, + }); + }, [ + projectId, + leadId, + triggerAutoEntriesCreate, + ]); + const leadPreview = leadPreviewData?.project?.lead?.leadPreview; const extractionStatus = leadPreviewData?.project?.lead?.extractionStatus; @@ -536,62 +613,72 @@ function LeftPane(props: Props) { activeClassName={styles.simplifiedTab} retainMount="lazy" > - {(leadPreview?.textExtract?.length ?? 0) > 0 ? ( - - ) : ( - - )} - message={( - (extractionStatus === 'PENDING' - || extractionStatus === 'STARTED') - ? 'Simplified text is currently being extracted from this source. Please retry after few minutes.' - : 'Oops! Either the source was empty or we couldn\'t extract its text.' - )} - errored={extractionStatus === 'FAILED'} - erroredEmptyIcon={( - - )} - erroredEmptyMessage="There was an when issue extracting simplified - text for this source." - actions={(extractionStatus === 'PENDING' || extractionStatus === 'STARTED') && ( - - )} - /> - )} + <> + + + {(leadPreview?.textExtract?.length ?? 0) > 0 ? ( + + ) : ( + + )} + message={( + (extractionStatus === 'PENDING' + || extractionStatus === 'STARTED') + ? 'Simplified text is currently being extracted from this source. Please retry after few minutes.' + : 'Oops! Either the source was empty or we couldn\'t extract its text.' + )} + errored={extractionStatus === 'FAILED'} + erroredEmptyIcon={( + + )} + erroredEmptyMessage="There was an when issue extracting simplified + text for this source." + actions={(extractionStatus === 'PENDING' || extractionStatus === 'STARTED') && ( + + )} + /> + )} + )} {!hideOriginalPreview && ( @@ -637,6 +724,13 @@ function LeftPane(props: Props) { /> + {autoEntriesModalShown && isDefined(projectId) && ( + + )}
); } diff --git a/app/views/EntryEdit/LeftPane/styles.css b/app/views/EntryEdit/LeftPane/styles.css index cefec76d1f..e3b035c498 100644 --- a/app/views/EntryEdit/LeftPane/styles.css +++ b/app/views/EntryEdit/LeftPane/styles.css @@ -31,7 +31,7 @@ background-color: var(--dui-color-background-information); padding: var(--dui-spacing-large); overflow: auto; - gap: var(--dui-spacing-small); + gap: var(--dui-spacing-medium); .switch { flex-shrink: 0; From 74ee993d0dc31058729618c7f9fff67cba82bb95 Mon Sep 17 00:00:00 2001 From: Subina Date: Tue, 28 Nov 2023 09:48:30 +0545 Subject: [PATCH 04/13] Data transformation for draft entries --- app/types/user.tsx | 4 +- .../LeftPane/AssistItem/AssistPopup/index.tsx | 4 +- .../LeftPane/AutoEntriesModal/index.tsx | 589 +++++++++++++++++- app/views/EntryEdit/LeftPane/index.tsx | 83 ++- .../Home/Assignment/AssignmentItem/index.tsx | 2 +- 5 files changed, 671 insertions(+), 11 deletions(-) diff --git a/app/types/user.tsx b/app/types/user.tsx index 7978737a5e..79f3002f52 100644 --- a/app/types/user.tsx +++ b/app/types/user.tsx @@ -36,12 +36,12 @@ export interface Assignment { displayName: string; email: string; }; - contentObjectDetails: { + contentObjectDetails?: { id: number; title: string; lead?: string; entry?: string; - }; + } | undefined; isDone: boolean; contentObjectType: 'lead' | 'entryreviewcomment' | 'entrycomment'; } diff --git a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx index 007552694b..5625b58788 100644 --- a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx +++ b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx @@ -34,6 +34,7 @@ interface Props { onChange: (val: SetValueArg, name: undefined) => void; error: Error | undefined; onEntryCreateButtonClick: () => void; + variant?: 'normal' | 'compact' | 'nlp'; // NOTE: Normal entry creation refers to entry created without use of // recommendations onNormalEntryCreateButtonClick: () => void; @@ -50,6 +51,7 @@ interface Props { function AssistPopup(props: Props) { const { className, + variant = 'nlp', leadId, value, onChange, @@ -153,7 +155,7 @@ function AssistPopup(props: Props) { recommendations={recommendations} emptyValueHidden addButtonHidden - variant="nlp" + variant={variant} /> )} diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index d801b13109..e8202405c8 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -1,14 +1,56 @@ -import React, { useMemo } from 'react'; -import { isNotDefined } from '@togglecorp/fujs'; +import React, { + useMemo, + useCallback, + useState, +} from 'react'; +import { + isNotDefined, + isDefined, + randomString, + noOp, + listToMap, +} from '@togglecorp/fujs'; import { gql, useQuery } from '@apollo/client'; import { Modal, + ListView, } from '@the-deep/deep-ui'; +import { type Framework } from '#components/entry/types'; +import { type GeoArea } from '#components/GeoMultiSelectInput'; +import { + mappingsSupportedWidgets, + isCategoricalMappings, + WidgetHint, + filterMatrix1dMappings, + filterMatrix2dMappings, + filterScaleMappings, + filterSelectMappings, + filterMultiSelectMappings, + filterOrganigramMappings, + type MappingsItem, +} from '#types/newAnalyticalFramework'; +import { + PartialEntryType, + PartialAttributeType, +} from '#components/entry/schema'; import { AutoEntriesForLeadQuery, AutoEntriesForLeadQueryVariables, } from '#generated/types'; +import AssistPopup from '../AssistItem/AssistPopup'; + +import { + createOrganigramAttr, + createMatrix1dAttr, + createMatrix2dAttr, + createScaleAttr, + createSelectAttr, + createMultiSelectAttr, + createGeoAttr, +} from '../AssistItem/utils'; + +const GEOLOCATION_DEEPL_MODEL_ID = 'geolocation'; const AUTO_ENTRIES_FOR_LEAD = gql` query AutoEntriesForLead( @@ -26,16 +68,217 @@ const AUTO_ENTRIES_FOR_LEAD = gql` excerpt predictionReceivedAt predictionStatus + predictions { + id + draftEntry + tag + dataTypeDisplay + dataType + category + isSelected + modelVersion + modelVersionDeeplModelId + prediction + threshold + value + } } } } } `; +interface EntryAttributes { + predictions: { + tags: string[]; + locations: GeoArea[]; + }; + mappings: MappingsItem[] | null | undefined; + filteredWidgets: NonNullable[number]['widgets'] + | NonNullable; +} + +function handleMappingsFetch(entryAttributes: EntryAttributes) { + const { + predictions, + mappings, + filteredWidgets, + } = entryAttributes; + + if (predictions.tags.length <= 0 && predictions.locations.length <= 0) { + // setMessageText('DEEP could not provide any recommendations for the selected text.'); + return {}; + } + + if (isNotDefined(filteredWidgets)) { + return {}; + } + + const matchedMappings = mappings + ?.filter(isCategoricalMappings) + .filter((m) => m.tag && predictions.tags.includes(m.tag)); + + const supportedGeoWidgets = mappings + ?.filter((mappingItem) => mappingItem.widgetType === 'GEO') + ?.map((mappingItem) => mappingItem.widget); + + const { + tempAttrs: recommendedAttributes, + tempHints: widgetsHints, + } = filteredWidgets.reduce( + ( + acc: { tempAttrs: PartialAttributeType[]; tempHints: WidgetHint[]; }, + widget, + ) => { + const { + tempAttrs: oldTempAttrs, + tempHints: oldTempHints, + } = acc; + + if (widget.widgetId === 'MATRIX1D') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMatrix1dMappings); + + const attr = createMatrix1dAttr(supportedTags, widget); + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + + if (widget.widgetId === 'MATRIX2D') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMatrix2dMappings); + + const attr = createMatrix2dAttr(supportedTags, widget); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if (widget.widgetId === 'SCALE') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterScaleMappings); + + const { + attr, + hints, + } = createScaleAttr(supportedTags, widget); + + const hintsWithInfo: WidgetHint | undefined = hints ? { + widgetPk: widget.id, + widgetType: 'SCALE', + hints, + } : undefined; + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: hintsWithInfo + ? [...oldTempHints, hintsWithInfo] + : oldTempHints, + }; + } + if (widget.widgetId === 'SELECT') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterSelectMappings); + + const { + attr, + hints, + } = createSelectAttr(supportedTags, widget); + + const hintsWithInfo: WidgetHint | undefined = hints ? { + widgetPk: widget.id, + widgetType: 'SELECT', + hints, + } : undefined; + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: hintsWithInfo + ? [...oldTempHints, hintsWithInfo] + : oldTempHints, + }; + } + if (widget.widgetId === 'MULTISELECT') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMultiSelectMappings); + + const attr = createMultiSelectAttr( + supportedTags, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if (widget.widgetId === 'ORGANIGRAM') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterOrganigramMappings); + + const attr = createOrganigramAttr( + supportedTags, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if ( + widget.widgetId === 'GEO' + && predictions.locations.length > 0 + && supportedGeoWidgets?.includes(widget.id) + ) { + const attr = createGeoAttr( + predictions.locations, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + return acc; + }, + { + tempAttrs: [], + tempHints: [], + }, + ); + + if (recommendedAttributes.length <= 0 && widgetsHints.length <= 0) { + // setMessageText( + // 'The provided recommendations for this text did not fit any tags in this project.', + // ); + return {}; + } + + return { + hints: widgetsHints, + recommendations: recommendedAttributes, + geoAreas: predictions.locations, + }; +} + +const entryKeySelector = (entry: PartialEntryType) => entry.clientId; + interface Props { onModalClose: () => void; projectId: string; leadId: string; + frameworkDetails: Framework; } function AutoEntriesModal(props: Props) { @@ -43,8 +286,231 @@ function AutoEntriesModal(props: Props) { onModalClose, projectId, leadId, + frameworkDetails, } = props; + const [ + geoAreaOptions, + setGeoAreaOptions, + ] = useState<{ + entryId: string; + geoAreas: GeoArea[] | undefined | null; + } | undefined>(); + + // const [messageText, setMessageText] = useState(); + + const [ + allRecommendations, + setAllRecommendations, + ] = useState | undefined>(undefined); + + const [allHints, setAllHints] = useState< + Record | undefined + >(undefined); + + const mappings = frameworkDetails?.predictionTagsMapping; + + const { + allWidgets, + filteredWidgets, + } = useMemo(() => { + const widgetsFromPrimary = frameworkDetails?.primaryTagging?.flatMap( + (item) => (item.widgets ?? []), + ) ?? []; + const widgetsFromSecondary = frameworkDetails?.secondaryTagging ?? []; + const widgets = [ + ...widgetsFromPrimary, + ...widgetsFromSecondary, + ]; + return { + allWidgets: widgets, + filteredWidgets: widgets.filter((w) => mappingsSupportedWidgets.includes(w.widgetId)), + }; + }, [ + frameworkDetails, + ]); + + /* + const handleMappingsFetchTest = useCallback( + (predictions: { tags: string[]; locations: GeoArea[]; }) => { + if (predictions.tags.length <= 0 && predictions.locations.length <= 0) { + setMessageText('DEEP could not provide any recommendations for the selected text.'); + return; + } + + setGeoAreaOptions(predictions.locations); + + const matchedMappings = mappings + ?.filter(isCategoricalMappings) + .filter((m) => m.tag && predictions.tags.includes(m.tag)); + + const supportedGeoWidgets = mappings + ?.filter((mappingItem) => mappingItem.widgetType === 'GEO') + ?.map((mappingItem) => mappingItem.widget); + + const { + tempAttrs: recommendedAttributes, + tempHints: widgetsHints, + } = filteredWidgets.reduce( + ( + acc: { tempAttrs: PartialAttributeType[]; tempHints: WidgetHint[]; }, + widget, + ) => { + const { + tempAttrs: oldTempAttrs, + tempHints: oldTempHints, + } = acc; + + if (widget.widgetId === 'MATRIX1D') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMatrix1dMappings); + + const attr = createMatrix1dAttr(supportedTags, widget); + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + + if (widget.widgetId === 'MATRIX2D') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMatrix2dMappings); + + const attr = createMatrix2dAttr(supportedTags, widget); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if (widget.widgetId === 'SCALE') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterScaleMappings); + + const { + attr, + hints, + } = createScaleAttr(supportedTags, widget); + + const hintsWithInfo: WidgetHint | undefined = hints ? { + widgetPk: widget.id, + widgetType: 'SCALE', + hints, + } : undefined; + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: hintsWithInfo + ? [...oldTempHints, hintsWithInfo] + : oldTempHints, + }; + } + if (widget.widgetId === 'SELECT') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterSelectMappings); + + const { + attr, + hints, + } = createSelectAttr(supportedTags, widget); + + const hintsWithInfo: WidgetHint | undefined = hints ? { + widgetPk: widget.id, + widgetType: 'SELECT', + hints, + } : undefined; + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: hintsWithInfo + ? [...oldTempHints, hintsWithInfo] + : oldTempHints, + }; + } + if (widget.widgetId === 'MULTISELECT') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterMultiSelectMappings); + + const attr = createMultiSelectAttr( + supportedTags, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if (widget.widgetId === 'ORGANIGRAM') { + const supportedTags = matchedMappings + ?.filter((m) => m.widget === widget.id) + .filter(filterOrganigramMappings); + + const attr = createOrganigramAttr( + supportedTags, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + if ( + widget.widgetId === 'GEO' + && predictions.locations.length > 0 + && supportedGeoWidgets?.includes(widget.id) + ) { + const attr = createGeoAttr( + predictions.locations, + widget, + ); + + return { + tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, + tempHints: oldTempHints, + }; + } + return acc; + }, + { + tempAttrs: [], + tempHints: [], + }, + ); + + if (recommendedAttributes.length <= 0 && widgetsHints.length <= 0) { + setMessageText( + 'The provided recommendations for this text did + not fit any tags in this project.', + ); + return; + } + setAllHints(widgetsHints); + setAllRecommendations(recommendedAttributes); + return { + hints: widgetsHints, + recommendations: recommendedAttributes, + + }; + }, + [ + mappings, + filteredWidgets, + ], + ); + */ + + const [ + draftEntries, + setDraftEntries, + ] = useState([]); + const autoEntriesVariables = useMemo(() => ({ projectId, leadIds: [leadId], @@ -54,22 +520,135 @@ function AutoEntriesModal(props: Props) { ]); const { - data: autoEntries, + loading: autoEntriesLoading, } = useQuery( AUTO_ENTRIES_FOR_LEAD, { skip: isNotDefined(autoEntriesVariables), variables: autoEntriesVariables, + onCompleted: (response) => { + const entries = response.project?.assistedTagging?.draftEntryByLeads; + const transformedEntries = (entries ?? [])?.map((entry) => { + const validPredictions = entry.predictions?.filter(isDefined); + const categoricalTags = validPredictions?.filter( + (prediction) => ( + prediction.modelVersionDeeplModelId !== GEOLOCATION_DEEPL_MODEL_ID + && prediction.isSelected + ), + ).map( + (prediction) => prediction.tag, + ).filter(isDefined) ?? []; + + const entryAttributeData: EntryAttributes = { + predictions: { + tags: categoricalTags, + locations: [], + }, + mappings, + filteredWidgets, + }; + + const { + hints: entryHints, + recommendations: entryRecommendations, + geoAreas: entryGeoAreas, + } = handleMappingsFetch(entryAttributeData); + + const entryId = randomString(); + const requiredEntry = { + clientId: entryId, + entryType: 'EXCERPT' as const, + lead: leadId, + excerpt: entry.excerpt, + droppedExcerpt: entry.excerpt, + attributes: entryRecommendations?.map((attr) => { + if (attr.widgetType !== 'GEO') { + return attr; + } + // NOTE: Selecting only the 1st recommendation + return ({ + ...attr, + data: { + value: attr?.data?.value.slice(0, 1) ?? [], + }, + }); + }), + }; + + return { + entryId, + geoLocations: entryGeoAreas, + recommendations: entryRecommendations, + hints: entryHints, + entry: requiredEntry, + }; + }); + const requiredDraftEntries = transformedEntries?.map( + (draftEntry) => draftEntry.entry, + ); + const entryRecommendations = listToMap( + transformedEntries, + (item) => item.entryId, + (item) => item.recommendations, + ); + const entryHints = listToMap( + transformedEntries, + (item) => item.entryId, + (item) => item.hints, + ); + const entryGeoAreas = listToMap( + transformedEntries, + (item) => item.entryId, + (item) => item.geoLocations, + ); + setDraftEntries(requiredDraftEntries); + setAllRecommendations(entryRecommendations); + setAllHints(entryHints); + setGeoAreaOptions(entryGeoAreas); + }, }, ); - console.log('here', autoEntries); + const rendererParams = useCallback((entryId: string, datum: PartialEntryType) => ({ + frameworkDetails, + value: datum, + onChange: noOp, + leadId, + hints: allHints?.[entryId], + recommendations: allRecommendations?.[entryId], + geoAreaOptions: undefined, + onEntryDiscardButtonClick: noOp, + onEntryCreateButtonClick: noOp, + onNormalEntryCreateButtonClick: noOp, + onGeoAreaOptionsChange: noOp, + predictionsLoading: false, + predictionsErrored: false, + messageText: undefined, + variant: 'normal' as const, + error: undefined, + }), [ + allHints, + allRecommendations, + frameworkDetails, + leadId, + ]); return ( - Auto entries here + ); } diff --git a/app/views/EntryEdit/LeftPane/index.tsx b/app/views/EntryEdit/LeftPane/index.tsx index 02e15000ab..ff4c6ad1ef 100644 --- a/app/views/EntryEdit/LeftPane/index.tsx +++ b/app/views/EntryEdit/LeftPane/index.tsx @@ -47,6 +47,8 @@ import { LeadPreviewForTextQueryVariables, CreateAutoDraftEntriesMutation, CreateAutoDraftEntriesMutationVariables, + AutoDraftEntriesStatusQuery, + AutoDraftEntriesStatusQueryVariables, } from '#generated/types'; import { PartialEntryType as EntryInput } from '#components/entry/schema'; @@ -93,6 +95,22 @@ const CREATE_AUTO_DRAFT_ENTRIES = gql` } `; +const AUTO_DRAFT_ENTRIES_STATUS = gql` + query AutoDraftEntriesStatus ( + $projectId: ID!, + $leadId: ID!, + ) { + project(id: $projectId) { + id + assistedTagging { + extractionStatusByLead(leadId: $leadId) { + autoEntryExtractionStatus + } + } + } + } +`; + const entryKeySelector = (e: EntryInput) => e.clientId; export type TabOptions = 'simplified' | 'original' | 'entries' | undefined; @@ -219,6 +237,65 @@ function LeftPane(props: Props) { }, ); + // FIXME: randomId is used to create different query variables after each poll + // so that apollo doesn't create unnecessary cache + const [randomId, setRandomId] = useState(randomString()); + + const autoEntryStatusVariables = useMemo(() => { + if (isNotDefined(projectId)) { + return undefined; + } + return ({ + leadId, + randomId, + projectId, + }); + }, [ + randomId, + leadId, + projectId, + ]); + + const { + data: autoEntryExtractionStatus, + refetch: retriggerEntryExtractionStatus, + } = useQuery( + AUTO_DRAFT_ENTRIES_STATUS, + { + skip: isNotDefined(autoEntryStatusVariables), + variables: autoEntryStatusVariables, + }, + ); + + const [draftEntriesLoading, setDraftEntriesLoading] = useState(false); + + // TODO: This polling calls two queries at a time. Fix this. + useEffect(() => { + const timeout = setTimeout( + () => { + const shouldPoll = autoEntryExtractionStatus?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus === 'PENDING'; + if (shouldPoll) { + setDraftEntriesLoading(true); + setRandomId(randomString()); + retriggerEntryExtractionStatus(); + } else { + setDraftEntriesLoading(false); + } + }, + 2000, + ); + + return () => { + clearTimeout(timeout); + }; + }, [ + autoEntryExtractionStatus?.project?.assistedTagging + ?.extractionStatusByLead?.autoEntryExtractionStatus, + leadId, + retriggerEntryExtractionStatus, + ]); + const [ triggerAutoEntriesCreate, ] = useMutation( @@ -242,13 +319,14 @@ function LeftPane(props: Props) { ); } }, - onError: () => { + onError: (error) => { alert.show( 'Failed to extract entries using NLP.', { variant: 'error', }, ); + showAutoEntriesModal(); }, }, ); @@ -724,11 +802,12 @@ function LeftPane(props: Props) { /> - {autoEntriesModalShown && isDefined(projectId) && ( + {autoEntriesModalShown && isDefined(projectId) && frameworkDetails && ( )} diff --git a/app/views/Home/Assignment/AssignmentItem/index.tsx b/app/views/Home/Assignment/AssignmentItem/index.tsx index 6386d1ff64..97ef179d29 100644 --- a/app/views/Home/Assignment/AssignmentItem/index.tsx +++ b/app/views/Home/Assignment/AssignmentItem/index.tsx @@ -55,7 +55,7 @@ function AssignmentItem(props: AssignmentItemProps) { )); } if (contentObjectType === 'entryreviewcomment' || contentObjectType === 'entrycomment') { - if (!projectDetails?.id && contentObjectDetails?.lead) { + if (!contentObjectDetails || !projectDetails?.id || !contentObjectDetails?.lead) { return ( an entry From d6bc30ec4b2bcb2a34dae9e37dfa5726069829fd Mon Sep 17 00:00:00 2001 From: Aditya Khatri Date: Thu, 30 Nov 2023 17:01:53 +0545 Subject: [PATCH 05/13] Rearrange queries and mutation for auto entries modal --- .../LeftPane/AutoEntriesModal/index.tsx | 380 +++++++++--------- app/views/EntryEdit/LeftPane/index.tsx | 154 +------ 2 files changed, 195 insertions(+), 339 deletions(-) diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index e8202405c8..795b877cfe 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -1,5 +1,6 @@ import React, { useMemo, + useEffect, useCallback, useState, } from 'react'; @@ -10,10 +11,17 @@ import { noOp, listToMap, } from '@togglecorp/fujs'; -import { gql, useQuery } from '@apollo/client'; +import { + gql, + useQuery, + useMutation, +} from '@apollo/client'; import { Modal, ListView, + useAlert, + Button, + Message, } from '@the-deep/deep-ui'; import { type Framework } from '#components/entry/types'; @@ -37,6 +45,10 @@ import { import { AutoEntriesForLeadQuery, AutoEntriesForLeadQueryVariables, + CreateAutoDraftEntriesMutation, + CreateAutoDraftEntriesMutationVariables, + AutoDraftEntriesStatusQuery, + AutoDraftEntriesStatusQueryVariables, } from '#generated/types'; import AssistPopup from '../AssistItem/AssistPopup'; @@ -58,6 +70,7 @@ const AUTO_ENTRIES_FOR_LEAD = gql` $leadIds: [ID!], ) { project(id: $projectId) { + id assistedTagging { draftEntryByLeads( filter: { @@ -88,6 +101,39 @@ const AUTO_ENTRIES_FOR_LEAD = gql` } `; +const CREATE_AUTO_DRAFT_ENTRIES = gql` + mutation CreateAutoDraftEntries ( + $projectId: ID!, + $leadId: ID!, + ) { + project(id: $projectId) { + id + assistedTagging { + autoDraftEntryCreate(data: {lead: $leadId}) { + ok + errors + } + } + } + } +`; + +const AUTO_DRAFT_ENTRIES_STATUS = gql` + query AutoDraftEntriesStatus ( + $projectId: ID!, + $leadId: ID!, + ) { + project(id: $projectId) { + id + assistedTagging { + extractionStatusByLead(leadId: $leadId) { + autoEntryExtractionStatus + } + } + } + } +`; + interface EntryAttributes { predictions: { tags: string[]; @@ -289,15 +335,124 @@ function AutoEntriesModal(props: Props) { frameworkDetails, } = props; + const alert = useAlert(); + const [ geoAreaOptions, setGeoAreaOptions, - ] = useState<{ - entryId: string; - geoAreas: GeoArea[] | undefined | null; - } | undefined>(); + ] = useState | undefined>(undefined); + + // FIXME: randomId is used to create different query variables after each poll + // so that apollo doesn't create unnecessary cache + const [randomId, setRandomId] = useState(randomString()); - // const [messageText, setMessageText] = useState(); + const autoEntryStatusVariables = useMemo(() => { + if (isNotDefined(projectId)) { + return undefined; + } + return ({ + leadId, + randomId, + projectId, + }); + }, [ + randomId, + leadId, + projectId, + ]); + + const [draftEntriesLoading, setDraftEntriesLoading] = useState(true); + + const { + data: autoEntryExtractionStatus, + refetch: retriggerEntryExtractionStatus, + } = useQuery( + AUTO_DRAFT_ENTRIES_STATUS, + { + skip: isNotDefined(autoEntryStatusVariables), + variables: autoEntryStatusVariables, + onCompleted: (response) => { + const status = response?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + if (status === 'SUCCESS') { + setDraftEntriesLoading(false); + } + }, + }, + ); + + const extractionStatus = autoEntryExtractionStatus?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + + // TODO: This polling calls two queries at a time. Fix this. + useEffect(() => { + const timeout = setTimeout( + () => { + const shouldPoll = autoEntryExtractionStatus?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus === 'PENDING'; + if (shouldPoll) { + setDraftEntriesLoading(true); + setRandomId(randomString()); + retriggerEntryExtractionStatus(); + } else { + setDraftEntriesLoading(false); + } + }, + 2000, + ); + + return () => { + clearTimeout(timeout); + }; + }, [ + autoEntryExtractionStatus?.project?.assistedTagging + ?.extractionStatusByLead?.autoEntryExtractionStatus, + leadId, + retriggerEntryExtractionStatus, + ]); + + const [ + triggerAutoEntriesCreate, + ] = useMutation( + CREATE_AUTO_DRAFT_ENTRIES, + { + onCompleted: (response) => { + const autoEntriesResponse = response?.project?.assistedTagging + ?.autoDraftEntryCreate; + if (autoEntriesResponse?.ok) { + retriggerEntryExtractionStatus(); + } else { + alert.show( + 'Failed to extract entries using NLP.', + { + variant: 'error', + }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to extract entries using NLP.', + { + variant: 'error', + }, + ); + }, + }, + ); + + const handleAutoExtractClick = useCallback(() => { + triggerAutoEntriesCreate({ + variables: { + projectId, + leadId, + }, + }); + }, [ + projectId, + leadId, + triggerAutoEntriesCreate, + ]); const [ allRecommendations, @@ -311,7 +466,6 @@ function AutoEntriesModal(props: Props) { const mappings = frameworkDetails?.predictionTagsMapping; const { - allWidgets, filteredWidgets, } = useMemo(() => { const widgetsFromPrimary = frameworkDetails?.primaryTagging?.flatMap( @@ -330,182 +484,6 @@ function AutoEntriesModal(props: Props) { frameworkDetails, ]); - /* - const handleMappingsFetchTest = useCallback( - (predictions: { tags: string[]; locations: GeoArea[]; }) => { - if (predictions.tags.length <= 0 && predictions.locations.length <= 0) { - setMessageText('DEEP could not provide any recommendations for the selected text.'); - return; - } - - setGeoAreaOptions(predictions.locations); - - const matchedMappings = mappings - ?.filter(isCategoricalMappings) - .filter((m) => m.tag && predictions.tags.includes(m.tag)); - - const supportedGeoWidgets = mappings - ?.filter((mappingItem) => mappingItem.widgetType === 'GEO') - ?.map((mappingItem) => mappingItem.widget); - - const { - tempAttrs: recommendedAttributes, - tempHints: widgetsHints, - } = filteredWidgets.reduce( - ( - acc: { tempAttrs: PartialAttributeType[]; tempHints: WidgetHint[]; }, - widget, - ) => { - const { - tempAttrs: oldTempAttrs, - tempHints: oldTempHints, - } = acc; - - if (widget.widgetId === 'MATRIX1D') { - const supportedTags = matchedMappings - ?.filter((m) => m.widget === widget.id) - .filter(filterMatrix1dMappings); - - const attr = createMatrix1dAttr(supportedTags, widget); - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: oldTempHints, - }; - } - - if (widget.widgetId === 'MATRIX2D') { - const supportedTags = matchedMappings - ?.filter((m) => m.widget === widget.id) - .filter(filterMatrix2dMappings); - - const attr = createMatrix2dAttr(supportedTags, widget); - - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: oldTempHints, - }; - } - if (widget.widgetId === 'SCALE') { - const supportedTags = matchedMappings - ?.filter((m) => m.widget === widget.id) - .filter(filterScaleMappings); - - const { - attr, - hints, - } = createScaleAttr(supportedTags, widget); - - const hintsWithInfo: WidgetHint | undefined = hints ? { - widgetPk: widget.id, - widgetType: 'SCALE', - hints, - } : undefined; - - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: hintsWithInfo - ? [...oldTempHints, hintsWithInfo] - : oldTempHints, - }; - } - if (widget.widgetId === 'SELECT') { - const supportedTags = matchedMappings - ?.filter((m) => m.widget === widget.id) - .filter(filterSelectMappings); - - const { - attr, - hints, - } = createSelectAttr(supportedTags, widget); - - const hintsWithInfo: WidgetHint | undefined = hints ? { - widgetPk: widget.id, - widgetType: 'SELECT', - hints, - } : undefined; - - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: hintsWithInfo - ? [...oldTempHints, hintsWithInfo] - : oldTempHints, - }; - } - if (widget.widgetId === 'MULTISELECT') { - const supportedTags = matchedMappings - ?.filter((m) => m.widget === widget.id) - .filter(filterMultiSelectMappings); - - const attr = createMultiSelectAttr( - supportedTags, - widget, - ); - - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: oldTempHints, - }; - } - if (widget.widgetId === 'ORGANIGRAM') { - const supportedTags = matchedMappings - ?.filter((m) => m.widget === widget.id) - .filter(filterOrganigramMappings); - - const attr = createOrganigramAttr( - supportedTags, - widget, - ); - - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: oldTempHints, - }; - } - if ( - widget.widgetId === 'GEO' - && predictions.locations.length > 0 - && supportedGeoWidgets?.includes(widget.id) - ) { - const attr = createGeoAttr( - predictions.locations, - widget, - ); - - return { - tempAttrs: attr ? [...oldTempAttrs, attr] : oldTempAttrs, - tempHints: oldTempHints, - }; - } - return acc; - }, - { - tempAttrs: [], - tempHints: [], - }, - ); - - if (recommendedAttributes.length <= 0 && widgetsHints.length <= 0) { - setMessageText( - 'The provided recommendations for this text did - not fit any tags in this project.', - ); - return; - } - setAllHints(widgetsHints); - setAllRecommendations(recommendedAttributes); - return { - hints: widgetsHints, - recommendations: recommendedAttributes, - - }; - }, - [ - mappings, - filteredWidgets, - ], - ); - */ - const [ draftEntries, setDraftEntries, @@ -524,7 +502,9 @@ function AutoEntriesModal(props: Props) { } = useQuery( AUTO_ENTRIES_FOR_LEAD, { - skip: isNotDefined(autoEntriesVariables), + skip: isNotDefined(extractionStatus) + || extractionStatus !== 'SUCCESS' + || isNotDefined(autoEntriesVariables), variables: autoEntriesVariables, onCompleted: (response) => { const entries = response.project?.assistedTagging?.draftEntryByLeads; @@ -616,7 +596,7 @@ function AutoEntriesModal(props: Props) { leadId, hints: allHints?.[entryId], recommendations: allRecommendations?.[entryId], - geoAreaOptions: undefined, + geoAreaOptions: geoAreaOptions?.[entryId], onEntryDiscardButtonClick: noOp, onEntryCreateButtonClick: noOp, onNormalEntryCreateButtonClick: noOp, @@ -631,23 +611,45 @@ function AutoEntriesModal(props: Props) { allRecommendations, frameworkDetails, leadId, + geoAreaOptions, ]); + const hideList = draftEntriesLoading + || extractionStatus === 'NONE'; + return ( + Recommend entries + + )} + messageShown + messageIconShown + borderBetweenItem + /> + ); diff --git a/app/views/EntryEdit/LeftPane/index.tsx b/app/views/EntryEdit/LeftPane/index.tsx index ff4c6ad1ef..6db38243ea 100644 --- a/app/views/EntryEdit/LeftPane/index.tsx +++ b/app/views/EntryEdit/LeftPane/index.tsx @@ -5,7 +5,7 @@ import { isDefined, randomString, } from '@togglecorp/fujs'; -import { gql, useQuery, useMutation } from '@apollo/client'; +import { gql, useQuery } from '@apollo/client'; import { Tabs, Container, @@ -45,10 +45,6 @@ import { UserContext } from '#base/context/UserContext'; import { LeadPreviewForTextQuery, LeadPreviewForTextQueryVariables, - CreateAutoDraftEntriesMutation, - CreateAutoDraftEntriesMutationVariables, - AutoDraftEntriesStatusQuery, - AutoDraftEntriesStatusQueryVariables, } from '#generated/types'; import { PartialEntryType as EntryInput } from '#components/entry/schema'; @@ -79,38 +75,6 @@ const LEAD_PREVIEW = gql` } `; -const CREATE_AUTO_DRAFT_ENTRIES = gql` - mutation CreateAutoDraftEntries ( - $projectId: ID!, - $leadId: ID!, - ) { - project(id: $projectId) { - assistedTagging { - autoDraftEntryCreate(data: {lead: $leadId}) { - ok - errors - } - } - } - } -`; - -const AUTO_DRAFT_ENTRIES_STATUS = gql` - query AutoDraftEntriesStatus ( - $projectId: ID!, - $leadId: ID!, - ) { - project(id: $projectId) { - id - assistedTagging { - extractionStatusByLead(leadId: $leadId) { - autoEntryExtractionStatus - } - } - } - } -`; - const entryKeySelector = (e: EntryInput) => e.clientId; export type TabOptions = 'simplified' | 'original' | 'entries' | undefined; @@ -237,117 +201,6 @@ function LeftPane(props: Props) { }, ); - // FIXME: randomId is used to create different query variables after each poll - // so that apollo doesn't create unnecessary cache - const [randomId, setRandomId] = useState(randomString()); - - const autoEntryStatusVariables = useMemo(() => { - if (isNotDefined(projectId)) { - return undefined; - } - return ({ - leadId, - randomId, - projectId, - }); - }, [ - randomId, - leadId, - projectId, - ]); - - const { - data: autoEntryExtractionStatus, - refetch: retriggerEntryExtractionStatus, - } = useQuery( - AUTO_DRAFT_ENTRIES_STATUS, - { - skip: isNotDefined(autoEntryStatusVariables), - variables: autoEntryStatusVariables, - }, - ); - - const [draftEntriesLoading, setDraftEntriesLoading] = useState(false); - - // TODO: This polling calls two queries at a time. Fix this. - useEffect(() => { - const timeout = setTimeout( - () => { - const shouldPoll = autoEntryExtractionStatus?.project - ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus === 'PENDING'; - if (shouldPoll) { - setDraftEntriesLoading(true); - setRandomId(randomString()); - retriggerEntryExtractionStatus(); - } else { - setDraftEntriesLoading(false); - } - }, - 2000, - ); - - return () => { - clearTimeout(timeout); - }; - }, [ - autoEntryExtractionStatus?.project?.assistedTagging - ?.extractionStatusByLead?.autoEntryExtractionStatus, - leadId, - retriggerEntryExtractionStatus, - ]); - - const [ - triggerAutoEntriesCreate, - ] = useMutation( - CREATE_AUTO_DRAFT_ENTRIES, - { - onCompleted: (response) => { - const autoEntriesResponse = response?.project?.assistedTagging - ?.autoDraftEntryCreate; - if (autoEntriesResponse?.ok) { - alert.show( - 'The entries extraction has started.', - { variant: 'success' }, - ); - showAutoEntriesModal(); - } else { - alert.show( - 'Failed to extract entries using NLP.', - { - variant: 'error', - }, - ); - } - }, - onError: (error) => { - alert.show( - 'Failed to extract entries using NLP.', - { - variant: 'error', - }, - ); - showAutoEntriesModal(); - }, - }, - ); - - const handleAutoExtractClick = useCallback(() => { - if (isNotDefined(projectId)) { - return; - } - - triggerAutoEntriesCreate({ - variables: { - projectId, - leadId, - }, - }); - }, [ - projectId, - leadId, - triggerAutoEntriesCreate, - ]); - const leadPreview = leadPreviewData?.project?.lead?.leadPreview; const extractionStatus = leadPreviewData?.project?.lead?.extractionStatus; @@ -694,8 +547,9 @@ function LeftPane(props: Props) { <> From 3a023d8f73282d6b62a4c2a6d8d106e900e2797c Mon Sep 17 00:00:00 2001 From: Aditya Khatri Date: Mon, 4 Dec 2023 16:02:50 +0545 Subject: [PATCH 06/13] Add ability to add entries from recommendations --- .../LeftPane/AssistItem/AssistPopup/index.tsx | 14 +- .../EntryEdit/LeftPane/AssistItem/index.tsx | 1 + .../LeftPane/AutoEntriesModal/index.tsx | 241 +++++++++++++----- .../LeftPane/AutoEntriesModal/styles.css | 7 + app/views/EntryEdit/LeftPane/index.tsx | 2 + 5 files changed, 202 insertions(+), 63 deletions(-) create mode 100644 app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css diff --git a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx index 5625b58788..dfaea0852f 100644 --- a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx +++ b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx @@ -26,12 +26,13 @@ import { import styles from './styles.css'; -interface Props { +interface Props { className?: string; + entryInputClassName?: string; frameworkDetails: Framework; leadId: string; value: PartialEntryType; - onChange: (val: SetValueArg, name: undefined) => void; + onChange: (val: SetValueArg, name: NAME) => void; error: Error | undefined; onEntryCreateButtonClick: () => void; variant?: 'normal' | 'compact' | 'nlp'; @@ -45,16 +46,19 @@ interface Props { hints: WidgetHint[] | undefined; recommendations: PartialAttributeType[] | undefined; predictionsErrored: boolean; + name: NAME; messageText: string | undefined; } -function AssistPopup(props: Props) { +function AssistPopup(props: Props) { const { className, + entryInputClassName, variant = 'nlp', leadId, value, onChange, + name, error, frameworkDetails, onEntryCreateButtonClick, @@ -138,9 +142,9 @@ function AssistPopup(props: Props) { /> ) : ( void + ) | undefined; } function AutoEntriesModal(props: Props) { @@ -332,10 +344,61 @@ function AutoEntriesModal(props: Props) { onModalClose, projectId, leadId, + onAssistedEntryAdd, frameworkDetails, + createdEntries, } = props; const alert = useAlert(); + const draftEntriesMap = useMemo(() => ( + listToMap( + createdEntries?.filter((item) => isDefined(item.draftEntry)) ?? [], + (item) => item.draftEntry ?? '', + () => true, + ) + ), [createdEntries]); + + const { + allWidgets, + filteredWidgets, + } = useMemo(() => { + const widgetsFromPrimary = frameworkDetails?.primaryTagging?.flatMap( + (item) => (item.widgets ?? []), + ) ?? []; + const widgetsFromSecondary = frameworkDetails?.secondaryTagging ?? []; + const widgets = [ + ...widgetsFromPrimary, + ...widgetsFromSecondary, + ]; + return { + allWidgets: widgets, + filteredWidgets: widgets.filter((w) => mappingsSupportedWidgets.includes(w.widgetId)), + }; + }, [ + frameworkDetails, + ]); + + const schema = useMemo( + () => { + const widgetsMapping = listToMap( + allWidgets, + (item) => item.id, + (item) => item, + ); + + return getSchema(widgetsMapping); + }, + [allWidgets], + ); + const { + setValue, + value, + setFieldValue, + } = useForm(schema, defaultFormValues); + + const { + setValue: onEntryChange, + } = useFormArray<'entries', PartialEntryType>('entries', setFieldValue); const [ geoAreaOptions, @@ -388,8 +451,7 @@ function AutoEntriesModal(props: Props) { useEffect(() => { const timeout = setTimeout( () => { - const shouldPoll = autoEntryExtractionStatus?.project - ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus === 'PENDING'; + const shouldPoll = extractionStatus === 'PENDING' || extractionStatus === 'STARTED'; if (shouldPoll) { setDraftEntriesLoading(true); setRandomId(randomString()); @@ -405,8 +467,7 @@ function AutoEntriesModal(props: Props) { clearTimeout(timeout); }; }, [ - autoEntryExtractionStatus?.project?.assistedTagging - ?.extractionStatusByLead?.autoEntryExtractionStatus, + extractionStatus, leadId, retriggerEntryExtractionStatus, ]); @@ -465,30 +526,6 @@ function AutoEntriesModal(props: Props) { const mappings = frameworkDetails?.predictionTagsMapping; - const { - filteredWidgets, - } = useMemo(() => { - const widgetsFromPrimary = frameworkDetails?.primaryTagging?.flatMap( - (item) => (item.widgets ?? []), - ) ?? []; - const widgetsFromSecondary = frameworkDetails?.secondaryTagging ?? []; - const widgets = [ - ...widgetsFromPrimary, - ...widgetsFromSecondary, - ]; - return { - allWidgets: widgets, - filteredWidgets: widgets.filter((w) => mappingsSupportedWidgets.includes(w.widgetId)), - }; - }, [ - frameworkDetails, - ]); - - const [ - draftEntries, - setDraftEntries, - ] = useState([]); - const autoEntriesVariables = useMemo(() => ({ projectId, leadIds: [leadId], @@ -540,6 +577,7 @@ function AutoEntriesModal(props: Props) { entryType: 'EXCERPT' as const, lead: leadId, excerpt: entry.excerpt, + draftEntry: entry.id, droppedExcerpt: entry.excerpt, attributes: entryRecommendations?.map((attr) => { if (attr.widgetType !== 'GEO') { @@ -581,7 +619,9 @@ function AutoEntriesModal(props: Props) { (item) => item.entryId, (item) => item.geoLocations, ); - setDraftEntries(requiredDraftEntries); + setValue({ + entries: requiredDraftEntries, + }); setAllRecommendations(entryRecommendations); setAllHints(entryHints); setGeoAreaOptions(entryGeoAreas); @@ -589,24 +629,100 @@ function AutoEntriesModal(props: Props) { }, ); - const rendererParams = useCallback((entryId: string, datum: PartialEntryType) => ({ - frameworkDetails, - value: datum, - onChange: noOp, - leadId, - hints: allHints?.[entryId], - recommendations: allRecommendations?.[entryId], - geoAreaOptions: geoAreaOptions?.[entryId], - onEntryDiscardButtonClick: noOp, - onEntryCreateButtonClick: noOp, - onNormalEntryCreateButtonClick: noOp, - onGeoAreaOptionsChange: noOp, - predictionsLoading: false, - predictionsErrored: false, - messageText: undefined, - variant: 'normal' as const, - error: undefined, - }), [ + const handleEntryCreateButtonClick = useCallback((entryId: string) => { + if (!allRecommendations?.[entryId]) { + return; + } + + const selectedEntry = value?.entries?.find((item) => item.clientId === entryId); + if (onAssistedEntryAdd && selectedEntry) { + const defaultAttributes = createDefaultAttributes(allWidgets); + + const newAttributes = mergeLists( + defaultAttributes, + selectedEntry?.attributes ?? [], + (attr) => attr.widget, + (defaultAttr, newAttr) => ({ + ...newAttr, + clientId: defaultAttr.clientId, + widget: defaultAttr.widget, + id: defaultAttr.id, + widgetVersion: defaultAttr.widgetVersion, + }), + ); + + onAssistedEntryAdd( + { + ...selectedEntry, + attributes: newAttributes, + }, + geoAreaOptions?.[entryId] ?? undefined, + ); + + alert.show( + 'Successfully added entry from recommendation.', + { + variant: 'success', + }, + ); + } else { + alert.show( + 'Failed to add entry from recommendations.', + { + variant: 'error', + }, + ); + } + }, [ + alert, + value?.entries, + allWidgets, + geoAreaOptions, + allRecommendations, + onAssistedEntryAdd, + ]); + + const filteredEntries = useMemo(() => ( + value?.entries?.filter( + (item) => item.draftEntry && !draftEntriesMap[item.draftEntry], + ) + ), [ + value?.entries, + draftEntriesMap, + ]); + + const rendererParams = useCallback(( + entryId: string, + datum: PartialEntryType, + ) => { + const onEntryCreateButtonClick = () => handleEntryCreateButtonClick(entryId); + const index = value?.entries?.findIndex((item) => item.clientId === entryId); + + return ({ + frameworkDetails, + value: datum, + className: styles.listItem, + entryInputClassName: styles.entryInput, + name: index, + onChange: onEntryChange, + leadId, + hints: allHints?.[entryId], + recommendations: allRecommendations?.[entryId], + geoAreaOptions: geoAreaOptions?.[entryId], + onEntryDiscardButtonClick: noOp, + onEntryCreateButtonClick, + onNormalEntryCreateButtonClick: noOp, + onGeoAreaOptionsChange: noOp, + predictionsLoading: false, + predictionsErrored: false, + messageText: undefined, + variant: 'normal' as const, + error: undefined, + }); + }, [ + value?.entries, + handleEntryCreateButtonClick, + onEntryChange, allHints, allRecommendations, frameworkDetails, @@ -614,26 +730,39 @@ function AutoEntriesModal(props: Props) { geoAreaOptions, ]); + const isFiltered = useMemo(() => ( + (filteredEntries?.length ?? 0) < (value?.entries?.length ?? 0) + ), [ + filteredEntries, + value?.entries, + ]); + const hideList = draftEntriesLoading + || autoEntriesLoading || extractionStatus === 'NONE'; return ( - ); } diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css b/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css new file mode 100644 index 0000000000..065f48366c --- /dev/null +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css @@ -0,0 +1,7 @@ +.modal-body { + .list-item { + .entry-input { + height: 360px; + } + } +} diff --git a/app/views/EntryEdit/LeftPane/index.tsx b/app/views/EntryEdit/LeftPane/index.tsx index 6db38243ea..7cee0ecd29 100644 --- a/app/views/EntryEdit/LeftPane/index.tsx +++ b/app/views/EntryEdit/LeftPane/index.tsx @@ -658,8 +658,10 @@ function LeftPane(props: Props) { {autoEntriesModalShown && isDefined(projectId) && frameworkDetails && ( From 5b7f4ba2abef5a016fd66561bc30e9b5d334c63f Mon Sep 17 00:00:00 2001 From: Subina Date: Wed, 6 Dec 2023 13:43:45 +0545 Subject: [PATCH 07/13] Make entry input similar to assisted tagging --- app/components/entry/EntryInput/index.tsx | 8 +++++++- app/components/entry/EntryInput/styles.css | 8 ++++++++ .../EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx | 6 ++++++ app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx | 4 +++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/components/entry/EntryInput/index.tsx b/app/components/entry/EntryInput/index.tsx index 735af63fc6..90d07459e3 100644 --- a/app/components/entry/EntryInput/index.tsx +++ b/app/components/entry/EntryInput/index.tsx @@ -81,6 +81,9 @@ interface EntryInputProps { allWidgets: Widget[] | undefined | null; rightComponent?: React.ReactNode; noPaddingInWidgetContainer?: boolean; + + excerptShown?: boolean; + displayHorizontally?: boolean; } function EntryInput(props: EntryInputProps) { @@ -111,6 +114,8 @@ function EntryInput(props: EntryInputProp onApplyToAll, rightComponent, noPaddingInWidgetContainer = false, + excerptShown = false, + displayHorizontally = false, } = props; const error = getErrorObject(riskyError); @@ -194,11 +199,12 @@ function EntryInput(props: EntryInputProp className={_cs( className, compactMode && styles.compact, + displayHorizontally && styles.horizontal, styles.entryInput, )} > - {!compactMode && ( + {(!compactMode || excerptShown) && ( { predictionsErrored: boolean; name: NAME; messageText: string | undefined; + excerptShown?: boolean; + displayHorizontally?: boolean; } function AssistPopup(props: Props) { @@ -71,6 +73,8 @@ function AssistPopup(props: Props { @@ -160,6 +164,8 @@ function AssistPopup(props: Props )} diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index 4493c4b39b..1e3db429da 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -716,8 +716,10 @@ function AutoEntriesModal(props: Props) { predictionsLoading: false, predictionsErrored: false, messageText: undefined, - variant: 'normal' as const, + variant: 'nlp' as const, error: undefined, + excerptShown: true, + displayHorizontally: true, }); }, [ value?.entries, From b176a436a37412a9d274769e80a555596fc0b031 Mon Sep 17 00:00:00 2001 From: Subina Date: Wed, 13 Dec 2023 10:07:28 +0545 Subject: [PATCH 08/13] Enable discard/undiscard entry - Disallow duplicate entries creation. - Remove selection of entry after creation from auto draft entries --- app/components/entry/EntryInput/index.tsx | 1 + app/components/entry/EntryInput/styles.css | 3 + .../LeftPane/AssistItem/AssistPopup/index.tsx | 37 +-- .../EntryEdit/LeftPane/AssistItem/index.tsx | 39 ++- .../LeftPane/AutoEntriesModal/index.tsx | 308 +++++++++++++++--- .../LeftPane/AutoEntriesModal/styles.css | 5 + app/views/EntryEdit/index.tsx | 14 +- 7 files changed, 326 insertions(+), 81 deletions(-) diff --git a/app/components/entry/EntryInput/index.tsx b/app/components/entry/EntryInput/index.tsx index 90d07459e3..48bc774b2b 100644 --- a/app/components/entry/EntryInput/index.tsx +++ b/app/components/entry/EntryInput/index.tsx @@ -250,6 +250,7 @@ function EntryInput(props: EntryInputProp styles.section, sectionContainerClassName, compactMode && styles.compact, + displayHorizontally && styles.horizontal, )} renderer={CompactSection} keySelector={sectionKeySelector} diff --git a/app/components/entry/EntryInput/styles.css b/app/components/entry/EntryInput/styles.css index dd68ab066b..86d0ff405c 100644 --- a/app/components/entry/EntryInput/styles.css +++ b/app/components/entry/EntryInput/styles.css @@ -69,6 +69,9 @@ &.compact { border-bottom: var(--dui-width-separator-thin) solid var(--dui-color-separator); } + &.horizontal { + border-bottom: unset; + } } .secondary-tagging { diff --git a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx index 1aeb173ba0..7aeab3323a 100644 --- a/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx +++ b/app/views/EntryEdit/LeftPane/AssistItem/AssistPopup/index.tsx @@ -4,16 +4,11 @@ import { Kraken, Container, Message, - QuickActionButton, } from '@the-deep/deep-ui'; import { SetValueArg, Error, } from '@togglecorp/toggle-form'; -import { - IoClose, -} from 'react-icons/io5'; -import { FiEdit2 } from 'react-icons/fi'; import EntryInput from '#components/entry/EntryInput'; import { GeoArea } from '#components/GeoMultiSelectInput'; @@ -34,12 +29,9 @@ interface Props { value: PartialEntryType; onChange: (val: SetValueArg, name: NAME) => void; error: Error | undefined; - onEntryCreateButtonClick: () => void; variant?: 'normal' | 'compact' | 'nlp'; // NOTE: Normal entry creation refers to entry created without use of // recommendations - onNormalEntryCreateButtonClick: () => void; - onEntryDiscardButtonClick: () => void; geoAreaOptions: GeoArea[] | undefined | null; onGeoAreaOptionsChange: React.Dispatch>; predictionsLoading?: boolean; @@ -50,6 +42,8 @@ interface Props { messageText: string | undefined; excerptShown?: boolean; displayHorizontally?: boolean; + + footerActions: React.ReactNode; } function AssistPopup(props: Props) { @@ -63,9 +57,6 @@ function AssistPopup(props: Props(props: Props { @@ -99,28 +91,7 @@ function AssistPopup(props: Props - - - - - - - - )} + footerQuickActions={footerActions} contentClassName={styles.body} > {isMessageShown ? ( diff --git a/app/views/EntryEdit/LeftPane/AssistItem/index.tsx b/app/views/EntryEdit/LeftPane/AssistItem/index.tsx index 865542478a..af2cbfa8b4 100644 --- a/app/views/EntryEdit/LeftPane/AssistItem/index.tsx +++ b/app/views/EntryEdit/LeftPane/AssistItem/index.tsx @@ -18,6 +18,7 @@ import { Container, } from '@the-deep/deep-ui'; import { IoClose } from 'react-icons/io5'; +import { FiEdit2 } from 'react-icons/fi'; import { GeoArea } from '#components/GeoMultiSelectInput'; import brainIcon from '#resources/img/brain.svg'; @@ -152,7 +153,11 @@ interface Props { className?: string; text: string; onAssistedEntryAdd: ( - (newEntry: EntryInput, locations?: GeoArea[]) => void + ( + newEntry: EntryInput, + locations?: GeoArea[], + selectCreatedEntry?: boolean, + ) => void ) | undefined; frameworkDetails?: Framework; leadId: string; @@ -641,6 +646,7 @@ function AssistItem(props: Props) { draftEntry: data?.project?.assistedTagging?.draftEntry?.id, }, geoAreaOptions ?? undefined, + true, ); } }, @@ -746,9 +752,6 @@ function AssistItem(props: Props) { leadId={leadId} hints={allHints} recommendations={allRecommendations} - onEntryDiscardButtonClick={handleDiscardButtonClick} - onEntryCreateButtonClick={handleEntryCreateButtonClick} - onNormalEntryCreateButtonClick={handleNormalEntryCreateButtonClick} geoAreaOptions={geoAreaOptions} onGeoAreaOptionsChange={setGeoAreaOptions} predictionsLoading={ @@ -758,6 +761,34 @@ function AssistItem(props: Props) { } predictionsErrored={!!fetchErrors || !!createErrors || isErrored} messageText={messageText} + footerActions={( + <> + + + + + + + + )} /> )} diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index 1e3db429da..3dacbbc298 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -23,6 +23,10 @@ import { import { Modal, ListView, + Tab, + Tabs, + TabPanel, + TabList, useAlert, Button, } from '@the-deep/deep-ui'; @@ -54,12 +58,11 @@ import { CreateAutoDraftEntriesMutationVariables, AutoDraftEntriesStatusQuery, AutoDraftEntriesStatusQueryVariables, + UpdateDraftEntryMutation, + UpdateDraftEntryMutationVariables, } from '#generated/types'; import AssistPopup from '../AssistItem/AssistPopup'; import { createDefaultAttributes } from '../../utils'; - -import styles from './styles.css'; - import { createOrganigramAttr, createMatrix1dAttr, @@ -70,20 +73,30 @@ import { createGeoAttr, } from '../AssistItem/utils'; +import styles from './styles.css'; + const GEOLOCATION_DEEPL_MODEL_ID = 'geolocation'; const AUTO_ENTRIES_FOR_LEAD = gql` query AutoEntriesForLead( $projectId: ID!, - $leadIds: [ID!], + $leadId: ID!, + $isDiscarded: Boolean, ) { project(id: $projectId) { id + lead(id: $leadId) { + draftEntryStat { + discardedDraftEnrty + undiscardedDraftEntry + } + } assistedTagging { draftEntryByLeads( filter: { - draftEntryType: AUTO, - lead: $leadIds, + draftEntryTypes: AUTO, + leads: $leadId, + isDiscarded: $isDiscarded, }) { id excerpt @@ -117,7 +130,7 @@ const CREATE_AUTO_DRAFT_ENTRIES = gql` project(id: $projectId) { id assistedTagging { - autoDraftEntryCreate(data: {lead: $leadId}) { + triggerAutoDraftEntry(data: {lead: $leadId}) { ok errors } @@ -142,6 +155,27 @@ const AUTO_DRAFT_ENTRIES_STATUS = gql` } `; +const UPDATE_DRAFT_ENTRY = gql` + mutation UpdateDraftEntry( + $projectId: ID!, + $draftEntryId: ID!, + $input: UpdateDraftEntryInputType!, + ){ + project(id: $projectId) { + id + assistedTagging { + updateDraftEntry( + data: $input, + id: $draftEntryId, + ) { + errors + ok + } + } + } + } +`; + interface EntryAttributes { predictions: { tags: string[]; @@ -328,6 +362,8 @@ function handleMappingsFetch(entryAttributes: EntryAttributes) { const entryKeySelector = (entry: PartialEntryType) => entry.clientId; +type EntriesTabType = 'extracted' | 'discarded'; + interface Props { onModalClose: () => void; projectId: string; @@ -358,6 +394,11 @@ function AutoEntriesModal(props: Props) { ) ), [createdEntries]); + const [ + selectedTab, + setSelectedTab, + ] = useState('extracted'); + const { allWidgets, filteredWidgets, @@ -479,7 +520,7 @@ function AutoEntriesModal(props: Props) { { onCompleted: (response) => { const autoEntriesResponse = response?.project?.assistedTagging - ?.autoDraftEntryCreate; + ?.triggerAutoDraftEntry; if (autoEntriesResponse?.ok) { retriggerEntryExtractionStatus(); } else { @@ -528,14 +569,18 @@ function AutoEntriesModal(props: Props) { const autoEntriesVariables = useMemo(() => ({ projectId, - leadIds: [leadId], + leadId, + isDiscarded: selectedTab === 'discarded', }), [ projectId, leadId, + selectedTab, ]); const { + data: autoEntries, loading: autoEntriesLoading, + refetch: retriggerAutoEntriesFetch, } = useQuery( AUTO_ENTRIES_FOR_LEAD, { @@ -543,6 +588,8 @@ function AutoEntriesModal(props: Props) { || extractionStatus !== 'SUCCESS' || isNotDefined(autoEntriesVariables), variables: autoEntriesVariables, + // TODO: This is due to caching issue in apollo. + notifyOnNetworkStatusChange: true, onCompleted: (response) => { const entries = response.project?.assistedTagging?.draftEntryByLeads; const transformedEntries = (entries ?? [])?.map((entry) => { @@ -629,6 +676,11 @@ function AutoEntriesModal(props: Props) { }, ); + const discardedEntriesCount = autoEntries?.project?.lead + ?.draftEntryStat?.discardedDraftEnrty ?? 0; + const undiscardedEntriesCount = autoEntries?.project?.lead + ?.draftEntryStat?.undiscardedDraftEntry ?? 0; + const handleEntryCreateButtonClick = useCallback((entryId: string) => { if (!allRecommendations?.[entryId]) { return; @@ -636,6 +688,20 @@ function AutoEntriesModal(props: Props) { const selectedEntry = value?.entries?.find((item) => item.clientId === entryId); if (onAssistedEntryAdd && selectedEntry) { + const duplicateEntryCheck = createdEntries?.find( + (entry) => entry.droppedExcerpt === selectedEntry.droppedExcerpt, + ); + + if (isDefined(duplicateEntryCheck)) { + alert.show( + 'Similar entry found. Failed to add entry from recommendations.', + { + variant: 'error', + }, + ); + return; + } + const defaultAttributes = createDefaultAttributes(allWidgets); const newAttributes = mergeLists( @@ -680,6 +746,79 @@ function AutoEntriesModal(props: Props) { geoAreaOptions, allRecommendations, onAssistedEntryAdd, + createdEntries, + ]); + + const [ + triggerUpdateDraftEntry, + ] = useMutation( + UPDATE_DRAFT_ENTRY, + { + onCompleted: (response) => { + const updateDraftEntryResponse = response?.project?.assistedTagging + ?.updateDraftEntry; + retriggerAutoEntriesFetch(); + if (updateDraftEntryResponse?.ok) { + alert.show( + 'Successfully changed the discard status.', + { + variant: 'success', + }, + ); + } else { + alert.show( + 'Failed to change the discard status.', + { + variant: 'error', + }, + ); + } + }, + onError: () => { + alert.show( + 'Failed to change the discard status.', + { + variant: 'error', + }, + ); + }, + }, + ); + + const handleUpdateDraftEntryClick = useCallback((entryId: string | undefined) => { + triggerUpdateDraftEntry({ + variables: { + projectId, + input: { + lead: leadId, + isDiscarded: true, + }, + // FIXME: Handle this better + draftEntryId: entryId ?? '', + }, + }); + }, [ + triggerUpdateDraftEntry, + leadId, + projectId, + ]); + + const handleUndiscardEntryClick = useCallback((entryId: string | undefined) => { + triggerUpdateDraftEntry({ + variables: { + projectId, + input: { + lead: leadId, + isDiscarded: false, + }, + // FIXME: Handle this better + draftEntryId: entryId ?? '', + }, + }); + }, [ + triggerUpdateDraftEntry, + leadId, + projectId, ]); const filteredEntries = useMemo(() => ( @@ -697,6 +836,36 @@ function AutoEntriesModal(props: Props) { ) => { const onEntryCreateButtonClick = () => handleEntryCreateButtonClick(entryId); const index = value?.entries?.findIndex((item) => item.clientId === entryId); + const footerActions = (selectedTab === 'extracted' ? ( +
+ + +
+ ) : ( + + )); return ({ frameworkDetails, @@ -709,17 +878,15 @@ function AutoEntriesModal(props: Props) { hints: allHints?.[entryId], recommendations: allRecommendations?.[entryId], geoAreaOptions: geoAreaOptions?.[entryId], - onEntryDiscardButtonClick: noOp, - onEntryCreateButtonClick, - onNormalEntryCreateButtonClick: noOp, onGeoAreaOptionsChange: noOp, predictionsLoading: false, predictionsErrored: false, messageText: undefined, - variant: 'nlp' as const, + variant: 'normal' as const, error: undefined, excerptShown: true, displayHorizontally: true, + footerActions, }); }, [ value?.entries, @@ -730,6 +897,9 @@ function AutoEntriesModal(props: Props) { frameworkDetails, leadId, geoAreaOptions, + handleUpdateDraftEntryClick, + handleUndiscardEntryClick, + selectedTab, ]); const isFiltered = useMemo(() => ( @@ -739,45 +909,101 @@ function AutoEntriesModal(props: Props) { value?.entries, ]); + /* const hideList = draftEntriesLoading || autoEntriesLoading || extractionStatus === 'NONE'; + */ return ( - + + + {`All Recommendations (${undiscardedEntriesCount})`} + + - Recommend entries - - )} - messageShown - messageIconShown - borderBetweenItem - /> + {`Discarded Recommendations (${discardedEntriesCount})`} + + + + + Recommend entries + + )} + messageShown + messageIconShown + borderBetweenItem + /> + + + + Recommend entries + + )} + messageShown + messageIconShown + borderBetweenItem + /> + + ); } diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css b/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css index 065f48366c..1abbc56c75 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/styles.css @@ -4,4 +4,9 @@ height: 360px; } } + + .footer-buttons { + display: flex; + gap: var(--dui-spacing-medium); + } } diff --git a/app/views/EntryEdit/index.tsx b/app/views/EntryEdit/index.tsx index 8f4f081c3d..b45e888c7e 100644 --- a/app/views/EntryEdit/index.tsx +++ b/app/views/EntryEdit/index.tsx @@ -907,8 +907,14 @@ function EntryEdit(props: Props) { ); const handleAssistedEntryAdd = useCallback( - (newValue: PartialEntryType, newGeoAreaOptions?: GeoArea[]) => { - createRestorePoint(); + ( + newValue: PartialEntryType, + newGeoAreaOptions?: GeoArea[], + selectCreatedEntry = false, + ) => { + if (selectCreatedEntry) { + createRestorePoint(); + } setFormFieldValue( (prevValue: PartialFormType['entries']) => [ ...(prevValue ?? []), @@ -919,7 +925,9 @@ function EntryEdit(props: Props) { ], 'entries', ); - setSelectedEntry(newValue.clientId); + if (selectCreatedEntry) { + setSelectedEntry(newValue.clientId); + } if (newGeoAreaOptions && newGeoAreaOptions.length > 0) { setGeoAreaOptions((oldAreas) => { const newAreas = unique([ From 46ed6e7c18eaf6a8800a4e20ebf984263ffd8795 Mon Sep 17 00:00:00 2001 From: Aditya Khatri Date: Tue, 19 Dec 2023 14:05:35 +0545 Subject: [PATCH 09/13] Fix empty message issues in auto extract popup --- .../LeftPane/AutoEntriesModal/index.tsx | 134 ++++++++++-------- .../LeftPane/AutoEntriesModal/styles.css | 24 ++++ app/views/EntryEdit/LeftPane/index.tsx | 1 + app/views/EntryEdit/LeftPane/styles.css | 4 + .../Framework/FrameworkDetail/index.tsx | 26 ++++ .../Framework/FrameworkDetail/styles.css | 6 + 6 files changed, 135 insertions(+), 60 deletions(-) diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index 3dacbbc298..7e1f1b2b60 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -446,21 +446,15 @@ function AutoEntriesModal(props: Props) { setGeoAreaOptions, ] = useState | undefined>(undefined); - // FIXME: randomId is used to create different query variables after each poll - // so that apollo doesn't create unnecessary cache - const [randomId, setRandomId] = useState(randomString()); - const autoEntryStatusVariables = useMemo(() => { if (isNotDefined(projectId)) { return undefined; } return ({ leadId, - randomId, projectId, }); }, [ - randomId, leadId, projectId, ]); @@ -469,12 +463,15 @@ function AutoEntriesModal(props: Props) { const { data: autoEntryExtractionStatus, - refetch: retriggerEntryExtractionStatus, + loading: extractionStatusLoading, + startPolling, + stopPolling, } = useQuery( AUTO_DRAFT_ENTRIES_STATUS, { skip: isNotDefined(autoEntryStatusVariables), variables: autoEntryStatusVariables, + notifyOnNetworkStatusChange: true, onCompleted: (response) => { const status = response?.project ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; @@ -488,33 +485,30 @@ function AutoEntriesModal(props: Props) { const extractionStatus = autoEntryExtractionStatus?.project ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; - // TODO: This polling calls two queries at a time. Fix this. useEffect(() => { - const timeout = setTimeout( - () => { - const shouldPoll = extractionStatus === 'PENDING' || extractionStatus === 'STARTED'; - if (shouldPoll) { - setDraftEntriesLoading(true); - setRandomId(randomString()); - retriggerEntryExtractionStatus(); - } else { - setDraftEntriesLoading(false); - } - }, - 2000, - ); + const extractionStatusInternal = autoEntryExtractionStatus?.project + ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; - return () => { - clearTimeout(timeout); - }; + const shouldPoll = extractionStatusInternal === 'PENDING' || extractionStatusInternal === 'STARTED'; + if (shouldPoll) { + setDraftEntriesLoading(true); + startPolling(3_000); + } else { + stopPolling(); + setDraftEntriesLoading(false); + } }, [ - extractionStatus, + startPolling, + stopPolling, + autoEntryExtractionStatus, leadId, - retriggerEntryExtractionStatus, ]); const [ triggerAutoEntriesCreate, + { + loading: autoDraftEntriesTriggerPending, + }, ] = useMutation( CREATE_AUTO_DRAFT_ENTRIES, { @@ -522,7 +516,8 @@ function AutoEntriesModal(props: Props) { const autoEntriesResponse = response?.project?.assistedTagging ?.triggerAutoDraftEntry; if (autoEntriesResponse?.ok) { - retriggerEntryExtractionStatus(); + setDraftEntriesLoading(true); + startPolling(3_000); } else { alert.show( 'Failed to extract entries using NLP.', @@ -578,7 +573,8 @@ function AutoEntriesModal(props: Props) { ]); const { - data: autoEntries, + previousData, + data: autoEntries = previousData, loading: autoEntriesLoading, refetch: retriggerAutoEntriesFetch, } = useQuery( @@ -618,6 +614,10 @@ function AutoEntriesModal(props: Props) { geoAreas: entryGeoAreas, } = handleMappingsFetch(entryAttributeData); + if (!entryHints && !entryRecommendations && !entryGeoAreas) { + return undefined; + } + const entryId = randomString(); const requiredEntry = { clientId: entryId, @@ -647,7 +647,7 @@ function AutoEntriesModal(props: Props) { hints: entryHints, entry: requiredEntry, }; - }); + }).filter(isDefined); const requiredDraftEntries = transformedEntries?.map( (draftEntry) => draftEntry.entry, ); @@ -909,17 +909,31 @@ function AutoEntriesModal(props: Props) { value?.entries, ]); - /* - const hideList = draftEntriesLoading - || autoEntriesLoading - || extractionStatus === 'NONE'; - */ + const isPending = autoEntriesLoading + || draftEntriesLoading + || autoDraftEntriesTriggerPending + || extractionStatusLoading; + + const emptyMessage = useMemo(() => { + if (extractionStatus === 'NONE') { + return "Looks like you've not triggered an extraction yet"; + } + if (extractionStatus === 'SUCCESS') { + return "Looks like there aren't any recommendations."; + } + if (extractionStatus === 'FAILED') { + return "Looks like DEEP couldn't generate extractions for this source."; + } + if (extractionStatus === 'PENDING' || extractionStatus === 'STARTED') { + return 'Please wait while we load the recommendations.'; + } + return ''; + }, [extractionStatus]); return ( @@ -927,37 +941,38 @@ function AutoEntriesModal(props: Props) { value={selectedTab} onChange={setSelectedTab} > - - - {`All Recommendations (${undiscardedEntriesCount})`} - - - {`Discarded Recommendations (${discardedEntriesCount})`} - - + {(isDefined(extractionStatus) && (extractionStatus !== 'NONE')) && ( + + + {`All Recommendations (${undiscardedEntriesCount})`} + + + {`Discarded Recommendations (${discardedEntriesCount})`} + + + )} <> - {(leadPreview?.textExtract?.length ?? 0) > 0 ? ( Date: Wed, 20 Dec 2023 17:38:17 +0545 Subject: [PATCH 12/13] Add pagination in automatic entries --- .../LeftPane/AutoEntriesModal/index.tsx | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index a018fc63b8..ba13317bc3 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -21,12 +21,13 @@ import { useMutation, } from '@apollo/client'; import { - Modal, ListView, + Modal, + Pager, Tab, - Tabs, - TabPanel, TabList, + TabPanel, + Tabs, useAlert, Button, } from '@the-deep/deep-ui'; @@ -82,33 +83,41 @@ const AUTO_ENTRIES_FOR_LEAD = gql` $projectId: ID!, $leadId: ID!, $isDiscarded: Boolean, + $page: Int, + $pageSize: Int, ) { project(id: $projectId) { id assistedTagging { - draftEntryByLeads( - filter: { + draftEntries( draftEntryTypes: AUTO, - leads: $leadId, + lead: $leadId, isDiscarded: $isDiscarded, - }) { - id - excerpt - predictionReceivedAt - predictionStatus - predictions { + page: $page, + pageSize: $pageSize, + ) { + page + pageSize + totalCount + results { id - draftEntry - tag - dataTypeDisplay - dataType - category - isSelected - modelVersion - modelVersionDeeplModelId - prediction - threshold - value + excerpt + predictionReceivedAt + predictionStatus + predictions { + id + draftEntry + tag + dataTypeDisplay + dataType + category + isSelected + modelVersion + modelVersionDeeplModelId + prediction + threshold + value + } } } } @@ -354,6 +363,8 @@ function handleMappingsFetch(entryAttributes: EntryAttributes) { }; } +const MAX_ITEMS_PER_PAGE = 20; + const entryKeySelector = (entry: PartialEntryType) => entry.clientId; type EntriesTabType = 'extracted' | 'discarded'; @@ -393,6 +404,8 @@ function AutoEntriesModal(props: Props) { setSelectedTab, ] = useState('extracted'); + const [activePage, setActivePage] = useState(1); + const { allWidgets, filteredWidgets, @@ -560,13 +573,17 @@ function AutoEntriesModal(props: Props) { projectId, leadId, isDiscarded: selectedTab === 'discarded', + page: activePage, + pageSize: MAX_ITEMS_PER_PAGE, }), [ projectId, leadId, selectedTab, + activePage, ]); const { + data: autoEntries, loading: autoEntriesLoading, refetch: retriggerAutoEntriesFetch, } = useQuery( @@ -579,7 +596,7 @@ function AutoEntriesModal(props: Props) { // TODO: This is due to caching issue in apollo. notifyOnNetworkStatusChange: true, onCompleted: (response) => { - const entries = response.project?.assistedTagging?.draftEntryByLeads; + const entries = response.project?.assistedTagging?.draftEntries?.results; const transformedEntries = (entries ?? [])?.map((entry) => { const validPredictions = entry.predictions?.filter(isDefined); const categoricalTags = validPredictions?.filter( @@ -1005,6 +1022,13 @@ function AutoEntriesModal(props: Props) { /> + ); } From 3235ac28e5fc1cfdd7401844360b4fdf4bd536cc Mon Sep 17 00:00:00 2001 From: Subina Date: Thu, 21 Dec 2023 14:58:52 +0545 Subject: [PATCH 13/13] Fix for api change in auto entry extraction status --- .../EntryEdit/LeftPane/AutoEntriesModal/index.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx index ba13317bc3..65f322e286 100644 --- a/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx +++ b/app/views/EntryEdit/LeftPane/AutoEntriesModal/index.tsx @@ -149,10 +149,10 @@ const AUTO_DRAFT_ENTRIES_STATUS = gql` ) { project(id: $projectId) { id - assistedTagging { - extractionStatusByLead(leadId: $leadId) { - autoEntryExtractionStatus - } + lead( + id: $leadId, + ) { + autoEntryExtractionStatus } } } @@ -481,7 +481,7 @@ function AutoEntriesModal(props: Props) { notifyOnNetworkStatusChange: true, onCompleted: (response) => { const status = response?.project - ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + ?.lead?.autoEntryExtractionStatus; if (status === 'SUCCESS') { setDraftEntriesLoading(false); } @@ -490,11 +490,11 @@ function AutoEntriesModal(props: Props) { ); const extractionStatus = autoEntryExtractionStatus?.project - ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + ?.lead?.autoEntryExtractionStatus; useEffect(() => { const extractionStatusInternal = autoEntryExtractionStatus?.project - ?.assistedTagging?.extractionStatusByLead?.autoEntryExtractionStatus; + ?.lead?.autoEntryExtractionStatus; const shouldPoll = extractionStatusInternal === 'PENDING' || extractionStatusInternal === 'STARTED'; if (shouldPoll) {