Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/pass-query-to-event-page #137

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/Cards/MeetingCard/MeetingCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ meetingSearchResult.args = {
tags: ["bike", "adu", "accessories", "rental"],
excerpt:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce interdum, lorem eget vestibulum tincidunt, augue eros gravida lectus, ut efficitur neque nisi eu metus.",
gram: "ipsum",
};
36 changes: 32 additions & 4 deletions src/components/Cards/MeetingCard/MeetingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from "react";
import React, { useMemo } from "react";
import styled from "@emotion/styled";
import Highlighter from "react-highlight-words";
import { removeStopwords } from "stopword";
import { TAG_CONNECTOR } from "../../../constants/StyleConstants";
import "@mozilla-protocol/core/protocol/css/protocol.css";
import { strings } from "../../../assets/LocalizedStrings";
import cleanText from "../../../utils/cleanText";

export type MeetingCardProps = {
/** The static poster image src of the event */
Expand All @@ -19,6 +22,10 @@ export type MeetingCardProps = {
tags: string[];
/** A context span if the event was found through searching */
excerpt?: string;
/** The highest value gram of the context span */
gram?: string;
/** The query used to find this meeting */
query?: string;
};

const Meeting = styled.section({
Expand All @@ -41,9 +48,24 @@ const MeetingCard = ({
committee,
tags,
excerpt,
gram,
query,
}: MeetingCardProps) => {
const tagString = tags.map((tag) => tag.toLowerCase()).join(TAG_CONNECTOR);

const searchWords = useMemo(() => {
const cleanedQuery = cleanText(query || "");
// Phrases that should be highlighted in the excerpt
const phrases = removeStopwords(cleanedQuery.split(" "));
if (gram && gram.length > 0) {
phrases.push(gram);
}
if (phrases.length === 0) {
return [];
}
return [new RegExp(`\\b(${phrases.join("|")})`, "g")];
}, [query, gram]);

return (
<Meeting className="mzp-c-card mzp-has-aspect-16-9">
<div className="mzp-c-card-block-link">
Expand All @@ -55,15 +77,21 @@ const MeetingCard = ({
<div className="mzp-c-card-tag">{strings.committee}</div>
<h2 className="mzp-c-card-title">{meetingDate}</h2>
<p className="mzp-c-card-desc">{committee}</p>
{excerpt ? (
{excerpt && (
<p
className="mzp-c-card-desc"
style={{
fontStyle: "italic",
marginTop: "1rem",
}}
>{`"${excerpt}"`}</p>
) : null}
>
<Highlighter
caseSensitive={false}
searchWords={searchWords}
textToHighlight={`"${excerpt}"`}
/>
</p>
)}
<p className="mzp-c-card-meta">{strings.keywords}</p>
<p className="mzp-c-card-desc">{tagString}</p>
</div>
Expand Down
30 changes: 24 additions & 6 deletions src/components/Details/TranscriptItem/TranscriptItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { FC, RefObject, RefAttributes, useRef, useImperativeHandle } from "react";
import React, { FC, RefObject, RefAttributes, useRef, useImperativeHandle, useMemo } from "react";
import { Link } from "react-router-dom";
import Highlighter from "react-highlight-words";
import { Popup } from "semantic-ui-react";
import { stem } from "stemr";
import { removeStopwords } from "stopword";
import styled from "@emotion/styled";

import { strings } from "../../../assets/LocalizedStrings";
Expand All @@ -10,6 +12,7 @@ import DocumentTextIcon from "../../Shared/DocumentTextIcon";
import PlayIcon from "../../Shared/PlayIcon";

import { fontSizes } from "../../../styles/fonts";
import cleanText from "../../../utils/cleanText";

import "@mozilla-protocol/core/protocol/css/protocol.css";

Expand Down Expand Up @@ -131,15 +134,30 @@ const TranscriptItem: FC<TranscriptItemProps> = ({
</DefaultAvatarContainer>
);

const searchWords = useMemo(() => {
const cleanedQuery = cleanText(searchQuery || "");
const tokenizedQuery = removeStopwords(cleanedQuery.split(" "));
if (!cleanedQuery || tokenizedQuery.length === 0) {
// no query or valid tokens to highlight
return [];
}
const stemmedQuery = tokenizedQuery.map((token) => stem(token));
// highlight the token or the stem
const regExps = tokenizedQuery.map(
(token, i) => new RegExp(`\\b(${token}|${stemmedQuery[i]})`, "g")
);
if (searchQuery && searchQuery.trim().length > 0) {
// highlight the original query too
regExps.push(new RegExp(searchQuery.trim(), "g"));
}
return regExps;
}, [searchQuery]);

return (
<div ref={transcriptItemRef}>
<Item>
<Text>
<Highlighter
searchWords={(searchQuery?.trim() || "").split(/\s+/g)}
autoEscape={true}
textToHighlight={text}
/>
<Highlighter caseSensitive={false} searchWords={searchWords} textToHighlight={text} />
</Text>
<Container hasMultipleActions={handleJumpToTranscript !== undefined}>
<Speaker>
Expand Down
53 changes: 41 additions & 12 deletions src/components/Details/TranscriptSearch/TranscriptSearch.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { ChangeEventHandler, FC, useState, useMemo } from "react";
import React, { ChangeEventHandler, FC, useState, useMemo, FormEventHandler } from "react";
import styled from "@emotion/styled";
import { strings } from "../../../assets/LocalizedStrings";
import TranscriptItems from "./TranscriptItems";
import { stem } from "stemr";
import { removeStopwords } from "stopword";

import TranscriptItems from "./TranscriptItems";
import { SentenceWithSessionIndex } from "../../../containers/EventContainer/types";

import { strings } from "../../../assets/LocalizedStrings";
import { fontSizes } from "../../../styles/fonts";
import { screenWidths } from "../../../styles/mediaBreakpoints";
import isSubstring from "../../../utils/isSubstring";
import cleanText from "../../../utils/cleanText";

const Container = styled.div({
display: "flex",
Expand Down Expand Up @@ -65,23 +67,50 @@ const TranscriptSearch: FC<TranscriptSearchProps> = ({
jumpToVideoClip,
jumpToTranscript,
}: TranscriptSearchProps) => {
// Update the query in the search bar as the user types
const [searchTerm, setSearchTerm] = useState<string>(searchQuery);
const onSearchChange: ChangeEventHandler<HTMLInputElement> = (event) =>
const onSearchChange: ChangeEventHandler<HTMLInputElement> = (event) => {
setSearchTerm(event.target.value);
};

// The query after a search form submit
const [searchedTerm, setSearchedTerm] = useState(searchQuery);
const onSearch: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setSearchedTerm(searchTerm);
};

const stemmedSentences = useMemo(() => {
return sentences.map(({ text }) => {
const cleanedText = cleanText(text);
const tokens = removeStopwords(cleanedText.split(" "));
const stems = tokens.map((token) => stem(token).toLowerCase());
return new Set(stems);
});
}, [sentences]);

//Update the visible sentences as the searched query changes
const visibleSentences = useMemo(() => {
return sentences.filter(({ text }) => isSubstring(text, searchTerm));
}, [sentences, searchTerm]);
if (!searchedTerm.trim()) {
return sentences;
}
const cleanedQuery = cleanText(searchedTerm);
const tokenizedQuery = removeStopwords(cleanedQuery.split(" "));
if (!cleanedQuery || tokenizedQuery.length === 0) {
// empty query or no valid tokens to search
return [];
}
const stemmedQuery = tokenizedQuery.map((token) => stem(token).toLowerCase());
return sentences.filter((_, i) => stemmedQuery.some((q) => stemmedSentences[i].has(q)));
}, [sentences, stemmedSentences, searchedTerm]);

return (
<Container>
<TitleContainer>
<div>{strings.search_transcript}</div>
{searchTerm && (
<div>{strings.number_of_results.replace("{number}", `${visibleSentences.length}`)}</div>
)}
<div>{strings.number_of_results.replace("{number}", `${visibleSentences.length}`)}</div>
</TitleContainer>
<form className="mzp-c-form" role="search">
<form className="mzp-c-form" role="search" onSubmit={onSearch}>
<input
style={{ width: "100%" }}
type="search"
Expand All @@ -92,7 +121,7 @@ const TranscriptSearch: FC<TranscriptSearchProps> = ({
</form>
<TranscriptContainer hasSearchResults={visibleSentences.length !== 0}>
<TranscriptItems
searchQuery={searchTerm}
searchQuery={searchedTerm}
sentences={visibleSentences}
jumpToVideoClip={jumpToVideoClip}
jumpToTranscript={jumpToTranscript}
Expand Down
9 changes: 7 additions & 2 deletions src/containers/CardsContainer/CardsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ export interface CardsContainerProps {
const CardsContainer: FC<CardsContainerProps> = ({ cards }: CardsContainerProps) => {
return (
<Container>
{cards.map(({ link, jsx }) => {
{cards.map(({ link, jsx, searchQuery }) => {
return (
<div key={link}>
<Link
to={link}
to={{
pathname: link,
state: {
query: searchQuery || "",
},
}}
style={{
textDecoration: "none",
color: colors.black,
Expand Down
4 changes: 4 additions & 0 deletions src/containers/CardsContainer/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ReactNode } from "react";

export interface Card {
//**The link pathname of the card */
link: string;
/**The jsx element of the card */
jsx: ReactNode;
/**The search query used to find the card */
searchQuery?: string;
}
1 change: 1 addition & 0 deletions src/containers/SearchContainer/SearchContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const SearchContainer: FC<SearchContainerData> = ({ searchState }: SearchContain
committee={renderableEvent.event.body?.name as string}
tags={renderableEvent.keyGrams}
excerpt={renderableEvent.selectedContextSpan}
//TODO: add the gram and queryRef.current
/>
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,11 @@ const SearchEventsContainer: FC<SearchEventsContainerData> = ({
committee={renderableEvent.event.body?.name as string}
tags={renderableEvent.keyGrams}
excerpt={renderableEvent.selectedContextSpan}
gram={renderableEvent.selectedGram}
query={searchQueryRef.current}
/>
),
searchQuery: searchQueryRef.current,
};
});
return (
Expand Down
34 changes: 15 additions & 19 deletions src/networking/EventSearchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Event from "../models/Event";
import { createError } from "../utils/createError";
import { getStorage, ref, getDownloadURL } from "@firebase/storage";
import { FirebaseConfig } from "../app/AppConfigContext";
import cleanText from "../utils/cleanText";

/**
* The primary return of searchEvents.
Expand All @@ -23,19 +24,22 @@ class MatchingEvent {
pureRelevance: number;
datetimeWeightedRelevance: number;
containedGrams: string[];
selectedGram: string;
selectedContextSpan: string;

constructor(
eventId: string,
pureRelevance: number,
datetimeWeightedRelevance: number,
containedGrams: string[],
selectedGram: string,
selectedContextSpan: string
) {
this.eventRef = `${COLLECTION_NAME.Event}/${eventId}`;
this.pureRelevance = pureRelevance;
this.datetimeWeightedRelevance = datetimeWeightedRelevance;
this.containedGrams = containedGrams;
this.selectedGram = selectedGram;
this.selectedContextSpan = selectedContextSpan;
}
}
Expand All @@ -50,6 +54,7 @@ export class RenderableEvent {
pureRelevance: number;
datetimeWeightedRelevance: number;
containedGrams: string[];
selectedGram: string;
selectedContextSpan: string;
keyGrams: string[];
staticThumbnailURL: string;
Expand All @@ -60,6 +65,7 @@ export class RenderableEvent {
pureRelevance: number,
datetimeWeightedRelevance: number,
containedGrams: string[],
selectedGram: string,
selectedContextSpan: string,
keyGrams: string[],
staticThumbnailURL: string,
Expand All @@ -69,6 +75,7 @@ export class RenderableEvent {
this.pureRelevance = pureRelevance;
this.datetimeWeightedRelevance = datetimeWeightedRelevance;
this.containedGrams = containedGrams;
this.selectedGram = selectedGram;
this.selectedContextSpan = selectedContextSpan;
this.keyGrams = keyGrams;
this.staticThumbnailURL = staticThumbnailURL;
Expand Down Expand Up @@ -104,23 +111,10 @@ export default class EventSearchService {
* Returns as an array of string instead of string to pass into ngrams
*/
cleanText(query: string): string[] {
// Replace new line and tab characters with a space
let cleanedQuery = query.replace(/[\t\n]+/g, " ");

// Replace common strings used by documents on backend
// Not _really_ needed here but a nice safety measure to match the alg
cleanedQuery = cleanedQuery.replace(/[\-\-]/, " ");

// Same as Python standard punctuation string
cleanedQuery = cleanedQuery.replace(/['!"#$%&\\'()\*+,\-\.\/:;<=>?@\[\\\]\^_`{|}~']/g, "");

// Remove extra spaces
cleanedQuery = cleanedQuery.replace(/\s{2,}/g, " ");

// Remove leading and trailing spaces
const cleanedQuery = cleanText(query);
// Remove stopwords
// Return as list of terms
return removeStopwords(cleanedQuery.trim().split(" "));
return removeStopwords(cleanedQuery.split(" "));
}

getStemmedGrams(query: string): string[] {
Expand Down Expand Up @@ -197,10 +191,10 @@ export default class EventSearchService {

// Unpack matchingGram to protect from undefined
let selectedContextSpan = "";
if (matchingGramWithHighestValue && matchingGramWithHighestValue.context_span) {
selectedContextSpan = matchingGramWithHighestValue.context_span;
} else {
selectedContextSpan = "";
let selectedGram = "";
if (matchingGramWithHighestValue) {
selectedContextSpan = matchingGramWithHighestValue?.context_span || "";
selectedGram = matchingGramWithHighestValue?.unstemmed_gram || "";
}

// Get grams found in event from query
Expand All @@ -217,6 +211,7 @@ export default class EventSearchService {
sumBy(matchingIndexedEventGrams, "value"),
sumBy(matchingIndexedEventGrams, "datetime_weighted_value"),
containedGrams,
selectedGram,
selectedContextSpan
)
);
Expand Down Expand Up @@ -268,6 +263,7 @@ export default class EventSearchService {
matchingEvent.pureRelevance,
matchingEvent.datetimeWeightedRelevance,
matchingEvent.containedGrams,
matchingEvent.selectedGram,
matchingEvent.selectedContextSpan,
keyUnstemmedGrams,
staticThumbnailPathURL,
Expand Down
Loading