From f8d1d628194e360330aaf4950d8d53e677f64630 Mon Sep 17 00:00:00 2001 From: Adam Mertel <12932677+adammertel@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:27:54 +0100 Subject: [PATCH] 2083 basic string search of full texts and opening the place in annotator (#2459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add basic search capabilities * Add search panel to header * Fix search scroll in annotator * Fix searching implementation --------- Co-authored-by: Ján Mertel <10438157+jancimertel@users.noreply.github.com> --- packages/annotator/src/index.html | 32 +++- packages/annotator/src/lib/Annotator.ts | 53 +++++++ .../advanced/Annotator/Annotator.tsx | 3 +- .../StatementListHeader.tsx | 6 +- .../StatementListHeaderStyles.tsx | 2 +- .../StatementListTextAnnotator.tsx | 137 +++++++++++++++++- 6 files changed, 220 insertions(+), 13 deletions(-) diff --git a/packages/annotator/src/index.html b/packages/annotator/src/index.html index 5490ec55b..901e72ce5 100644 --- a/packages/annotator/src/index.html +++ b/packages/annotator/src/index.html @@ -62,6 +62,12 @@ +
+ + + + +
- +
@@ -109,6 +115,8 @@ const exampleResponse = await fetch("example.txt"); const exampleTxt = await exampleResponse.text(); + const searchInput = document.getElementById("search"); + const searchTriggerInput = document.getElementById("search-trigger"); let highlightEnabled = false; const annotator = new Annotator( @@ -171,6 +179,7 @@ }, }; }); + annotator.onSelectText((text) => { // console.log("onSelectText", text); }); @@ -205,6 +214,27 @@ annotator.updateText(document.getElementById("newText").value); }; + let searchOccurence = 0; + document.getElementById("searchTriggerNext").onclick = () => { + const results = annotator.search(searchInput.value); + if (results.length) { + searchOccurence = searchOccurence + 1; + + if (searchOccurence >= results.length) { + searchOccurence = 0; + } + + annotator.selectSearchOccurence(results[searchOccurence]); + } + }; + + searchTriggerInput.onclick = function () { + const results = annotator.search(searchInput.value); + if (results.length) { + annotator.selectSearchOccurence(results[0]); + } + }; + window.addEventListener("resize", resizeAnnotator); function resizeAnnotator() { diff --git a/packages/annotator/src/lib/Annotator.ts b/packages/annotator/src/lib/Annotator.ts index 44fcdfc86..bf0518908 100644 --- a/packages/annotator/src/lib/Annotator.ts +++ b/packages/annotator/src/lib/Annotator.ts @@ -1260,4 +1260,57 @@ export class Annotator { this.scrollToLine(this.text.noLines - 1); } } + + search( + toFind: string + ): { segmentIndex: number; lineIndex: number; start: number; end: number }[] { + const occurrences = []; + + for (const segmentI in this.text.segments) { + for (const lineI in this.text.segments[segmentI].lines) { + let startIndex = 0; + const line = this.text.segments[segmentI].lines[lineI]; + while (startIndex < line.length) { + const index = line.indexOf(toFind, startIndex); + if (index === -1) break; + + occurrences.push({ + segmentIndex: parseInt(segmentI), + lineIndex: parseInt(lineI), + start: index, + end: index + toFind.length, + }); + startIndex = index + 1; + } + } + } + + return occurrences; + } + + selectSearchOccurence(occurence: { + segmentIndex: number; + lineIndex: number; + start: number; + end: number; + }) { + this.cursor.selectStart = { + xLine: occurence.start, + yLine: + this.text.segments[occurence.segmentIndex].lineStart + + occurence.lineIndex, + }; + this.cursor.selectEnd = { + xLine: occurence.end, + yLine: + this.text.segments[occurence.segmentIndex].lineStart + + occurence.lineIndex, + }; + + // this.cursor.xLine = this.cursor.selectStart.xLine; + // this.cursor.yLine = this.cursor.selectStart.yLine - this.viewport.lineStart; + + this.scrollToLine(this.cursor.selectStart.yLine); + this.draw(); + } } diff --git a/packages/client/src/components/advanced/Annotator/Annotator.tsx b/packages/client/src/components/advanced/Annotator/Annotator.tsx index b20a76739..ade26754a 100644 --- a/packages/client/src/components/advanced/Annotator/Annotator.tsx +++ b/packages/client/src/components/advanced/Annotator/Annotator.tsx @@ -6,6 +6,7 @@ import { toast } from "react-toastify"; import { v4 as uuidv4 } from "uuid"; import { Annotator, EditMode } from "@inkvisitor/annotator/src/lib"; +import { EntityEnums } from "@shared/enums"; import { IDocument, IEntity, IResponseDocumentDetail } from "@shared/types"; import { Button } from "components/basic/Button/Button"; import { ButtonGroup } from "components/basic/ButtonGroup/ButtonGroup"; @@ -23,7 +24,6 @@ import { StyledScrollerViewport, } from "./AnnotatorStyles"; import { annotatorHighlight } from "./highlight"; -import { EntityEnums } from "@shared/enums"; interface TextAnnotatorProps { width: number; @@ -509,6 +509,7 @@ export const TextAnnotator = ({ }} /> )} + setIsSelectingText(true)} onMouseUp={() => setIsSelectingText(false)} diff --git a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeader.tsx b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeader.tsx index eb1505eca..92e93666b 100644 --- a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeader.tsx +++ b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeader.tsx @@ -54,7 +54,7 @@ import { StyledHeaderBreadcrumbRow, StyledHeaderRow, StyledHeading, - StyledModeSwitcher, + StyledInfoText, StyledMoveToParent, StyledSuggesterRow, } from "./StatementListHeaderStyles"; @@ -508,7 +508,7 @@ export const StatementListHeader: React.FC = ({ )} - + {"Mode "} - + diff --git a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeaderStyles.tsx b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeaderStyles.tsx index 1a99d8ff8..f5909e39a 100644 --- a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeaderStyles.tsx +++ b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListHeader/StatementListHeaderStyles.tsx @@ -39,7 +39,7 @@ export const StyledFaStar = styled(FaStar)` margin-right: 0.5rem; color: ${({ theme }) => theme.color["warning"]}; `; -export const StyledModeSwitcher = styled.div` +export const StyledInfoText = styled.div` font-size: ${({ theme }) => theme.fontSize["sm"]}; font-weight: ${({ theme }) => theme.fontWeight["normal"]}; display: flex; diff --git a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx index 232b40abc..1534a84c9 100644 --- a/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx +++ b/packages/client/src/pages/Main/containers/StatementsListBox/StatementListTextAnnotator/StatementListTextAnnotator.tsx @@ -4,7 +4,7 @@ import { EntityEnums, UserEnums } from "@shared/enums"; import { IEntity, IResponseEntity, IResponseStatement } from "@shared/types"; import { useQuery } from "@tanstack/react-query"; import api from "api"; -import { Button, Loader } from "components"; +import { Button, Input, Loader } from "components"; import Dropdown, { EntitySuggester, EntityTag } from "components/advanced"; import TextAnnotator from "components/advanced/Annotator/Annotator"; import AnnotatorProvider from "components/advanced/Annotator/AnnotatorProvider"; @@ -16,12 +16,13 @@ import { TiDocumentText } from "react-icons/ti"; import { ThemeContext } from "styled-components"; import { COLLAPSED_TABLE_WIDTH } from "Theme/constants"; import useResizeObserver from "use-resize-observer"; -import { StyledModeSwitcher } from "../StatementListHeader/StatementListHeaderStyles"; +import { StyledInfoText } from "../StatementListHeader/StatementListHeaderStyles"; import { StyledDocumentTag, StyledDocumentTitle, } from "../StatementLitBoxStyles"; import { entitiesDict } from "@shared/dictionaries/entity"; +import { useDebounce } from "hooks"; interface StatementListTextAnnotator { statements: IResponseStatement[]; @@ -82,6 +83,52 @@ export const StatementListTextAnnotator: React.FC< }, []); const [annotator, setAnnotator] = useState(undefined); + const [searchTerm, setSearchTerm] = useState(""); + const [searchOccurences, setSearchOccurences] = useState< + { segmentIndex: number; lineIndex: number; start: number; end: number }[] + >([]); + const [searchActiveOccurence, setSearchActiveOccurence] = useState(0); + + useEffect(() => { + const newSelectedOccurence = searchOccurences[searchActiveOccurence]; + + console.log( + `selecting ${searchActiveOccurence} of`, + searchOccurences, + ` => ${newSelectedOccurence}` + ); + + if (newSelectedOccurence) { + annotator?.selectSearchOccurence(newSelectedOccurence); + } + }, [searchActiveOccurence, searchOccurences]); + + const dSearchTerm = useDebounce(searchTerm, 1000); + + const isSearchTermValid = useMemo(() => { + return dSearchTerm.length > 2; + }, [dSearchTerm]); + + useEffect(() => { + if (annotator) { + if (isSearchTermValid) { + annotator?.search(searchTerm); + const occurences = annotator?.search(searchTerm); + + setSearchOccurences(occurences); + + setTimeout(() => { + setSearchActiveOccurence(0); + }, 1000); + + // if (occurences.length > 0) { + // annotator?.selectSearchOccurence( + // searchOccurences[searchActiveOccurence] + // ); + // } + } + } + }, [dSearchTerm]); const animatedStyle = useSpring({ opacity: showAnnotator ? 1 : 0, @@ -213,6 +260,22 @@ export const StatementListTextAnnotator: React.FC< const themeContext = useContext(ThemeContext); + const isSearchAllowed = useMemo(() => { + return annotator !== undefined && selectedDocument !== undefined; + }, [annotator, selectedDocument]); + + const annotatorHeight = useMemo(() => { + let height = contentHeight - 70; + + if (isSearchAllowed) { + height -= 30; + } + if (selectorHeight) { + height -= selectorHeight; + } + return height; + }, [contentHeight, selectorHeight, isSearchAllowed]); + return (
- + Highlight - + )} + {isSearchAllowed && ( +
+ + Search + + { + setSearchTerm(newText); + }} + changeOnType + /> + {isSearchTermValid && ( +
+
+ {searchActiveOccurence + 1} / {searchOccurences.length}{" "} + occurences +
+
+ )} +
+ )} + {/* Annotator */}
@@ -367,7 +490,7 @@ export const StatementListTextAnnotator: React.FC< thisTerritoryEntityId={territoryId} initialScrollEntityId={territoryId} displayLineNumbers={true} - height={contentHeight - selectorHeight - 70} + height={annotatorHeight} documentId={selectedDocumentId} handleCreateStatement={handleCreateStatement} handleCreateTerritory={handleCreateTerritory}