Skip to content

Commit

Permalink
Search Page: Breadcrumb links & advanced search
Browse files Browse the repository at this point in the history
  • Loading branch information
myluki2000 committed Sep 18, 2024
1 parent e627829 commit 781f4fd
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 153 deletions.
7 changes: 4 additions & 3 deletions app/search/SearchResultGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,17 @@ export default function SearchResultGroup({ searchResults, collapsedResultCount
{isExpanded ? <ExpandLess /> : <ExpandMore />}
</IconButton>
</Tooltip>
<Link color="inherit">
<Link color="inherit" href={`/courses/${userAccessibleContent.metadata.course.id}`}>
{userAccessibleContent.metadata.course.title}
</Link>
<Link color="inherit">
{userAccessibleContent.metadata.chapter.title}
</Link>
<Link color="inherit">
<Link color="inherit" href={`/course/${userAccessibleContent.metadata.course.id}/media/${userAccessibleContent.id}`}>
{userAccessibleContent.metadata.name}
</Link>
<Link color="inherit">
<Link color="inherit"
href={`/course/${userAccessibleContent.metadata.course.id}/media/${userAccessibleContent.id}?recordId=${mediaRecord.id}`}>
{mediaRecord.name}
</Link>
</Breadcrumbs>
Expand Down
181 changes: 120 additions & 61 deletions app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"use client";

import { pageSemanticSearchQuery, pageSemanticSearchQuery$data } from "@/__generated__/pageSemanticSearchQuery.graphql";
import { Box, Button, Collapse, IconButton, InputAdornment, TextField, Typography } from "@mui/material";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { Autocomplete, Box, Button, Collapse, IconButton, InputAdornment, Paper, TextField, Typography } from "@mui/material";
import { useRouter, useSearchParams } from "next/navigation";
import { graphql, useLazyLoadQuery } from "react-relay";
import SearchResult from "./SearchResult";
import SearchResultGroup from "./SearchResultGroup";
import {ManageSearch, ExpandMore, ExpandLess, Search} from '@mui/icons-material';
import { useState } from "react";
Expand All @@ -14,69 +13,85 @@ export default function SearchPage() {
const searchParams = useSearchParams();
const query: string | null = searchParams.get("query");

let semanticSearchResultGroups: (pageSemanticSearchQuery$data["semanticSearch"][0][] | undefined)[] = [];

if(query !== null) {
const { semanticSearch } = useLazyLoadQuery<pageSemanticSearchQuery>(
graphql`
query pageSemanticSearchQuery($query: String!) {
semanticSearch(queryText: $query, count: 40) {
score
mediaRecordSegment {
__typename
id
thumbnail
mediaRecordId
...on VideoRecordSegment {
startTime
screenText
transcript
mediaRecord {
id
name
type
contents {
metadata {
name
chapter {
title
}
course {
title
}
const { semanticSearch, currentUserInfo } = useLazyLoadQuery<pageSemanticSearchQuery>(
graphql`
query pageSemanticSearchQuery($query: String!, $count: Int!, $skipQuery: Boolean!, $courseWhitelist: [UUID!]) {
semanticSearch(queryText: $query, count: $count, courseWhitelist: $courseWhitelist) @skip(if: $skipQuery) {
score
mediaRecordSegment {
__typename
id
thumbnail
mediaRecordId
...on VideoRecordSegment {
startTime
screenText
transcript
mediaRecord {
id
name
type
contents {
metadata {
name
chapter {
title
}
course {
title
}
}
}
}
...on DocumentRecordSegment {
page
text
mediaRecord {
}
...on DocumentRecordSegment {
page
text
mediaRecord {
id
name
type
contents {
id
name
type
contents {
metadata {
name
chapter {
title
}
course {
title
}
metadata {
name
chapter {
id
title
}
course {
id
title
}
}
}
}
}
}
}
`,
{ query }
);

semanticSearchResultGroups = Object.values(Object.groupBy(semanticSearch, (result) => result.mediaRecordSegment.mediaRecord?.id ?? "unknown"));
}
currentUserInfo {
id
availableCourseMemberships {
course {
id
title
}
}
}
}
`,
{
query: query ?? "",
count: searchParams.get("count") ? parseInt(searchParams.get("count")!) : 20,
courseWhitelist: searchParams.get("courses") ? searchParams.get("courses")!.split(",") : null,
skipQuery: query === null
}
);

// Sort the results into groups based on the media record they belong to. semanticSearch might be null if no query text was
// provided, use an empty array in that case.
const semanticSearchResultGroups = Object.values(
Object.groupBy(semanticSearch ?? [], (result) => result.mediaRecordSegment.mediaRecord?.id ?? "unknown"));

// sort the elements in each group by score from lowest to highest
semanticSearchResultGroups.forEach((group) => {
Expand All @@ -86,18 +101,22 @@ export default function SearchPage() {

// sort the groups themselves by the score of the first element in each group
semanticSearchResultGroups.sort((a, b) => {
let aScore = a === undefined ? 99999 : a[0].score;
let bScore = b === undefined ? 99999 : b[0].score;
let aScore = a?.[0].score ?? 99999;
let bScore = b?.[0].score ?? 99999;
return aScore - bScore;
});

const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(false);
// open advanced search by default if an advanced search parameter was provided
const [isAdvancedSearchOpen, setIsAdvancedSearchOpen] = useState(searchParams.get("courses") !== null || searchParams.get("count") !== null);
function toggleAdvancedSearch() {
setIsAdvancedSearchOpen(!isAdvancedSearchOpen);
}

const [queryTextFieldValue, setQueryTextFieldValue] = useState(query);

const [advancedSearchSelectedCourses, setAdvancedSearchSelectedCourses] = useState<any[]>([]);
const [advancedSearchResultCount, setAdvancedSearchResultCount] = useState(20);

return (
<main>
<Typography variant="h1">Search</Typography>
Expand All @@ -107,20 +126,57 @@ export default function SearchPage() {
variant="outlined"
label="Search Query"
fullWidth
sx={{my: "10px"}}
sx={{my: "8px"}}
value={queryTextFieldValue}
onChange={(e) => setQueryTextFieldValue(e.target.value)}
onKeyDown={(e) => {
if(e.key === "Enter") {
router.push(`/search?query=${queryTextFieldValue}`);
}
}}
InputLabelProps={{shrink: true}}
InputProps={{
endAdornment: <InputAdornment position="end">
<IconButton
color="primary"
aria-label="search"
onClick={(e) => router.push(`/search?query=${queryTextFieldValue}`)}>
onClick={(e) => {
// if advanced search is open, pass the advanced search parameters, otherwise don't
if(isAdvancedSearchOpen) {
let url = `/search?query=${queryTextFieldValue}&count=${advancedSearchResultCount}`;
if(advancedSearchSelectedCourses.length > 0) {
url += `&courses=${advancedSearchSelectedCourses.map(x => x.id).join(",")}`;
}
router.push(url)
} else {
router.push(`/search?query=${queryTextFieldValue}`)
}
}}>
<Search />
</IconButton>
</InputAdornment>,
}} />
<Collapse in={isAdvancedSearchOpen}>

<Paper
variant="outlined"
sx={{padding: "16px", my: "8px"}}>
<Autocomplete
multiple
sx={{my: "8px"}}
options={currentUserInfo?.availableCourseMemberships.map(x => x.course) ?? []}
getOptionLabel={(option) => option.title}
value={advancedSearchSelectedCourses}
onChange={(_, value) => setAdvancedSearchSelectedCourses(value)}
renderInput={(params) => <TextField {...params} InputLabelProps={{shrink: true}} label="Limit search to courses" />}
/>
<TextField
variant="outlined"
type="number"
label="Number of results"
value={advancedSearchResultCount}
onChange={(e) => setAdvancedSearchResultCount(parseInt(e.target.value))}
InputLabelProps={{shrink: true}} />
</Paper>
</Collapse>
<Button
variant="outlined"
Expand All @@ -133,7 +189,10 @@ export default function SearchPage() {
{((query !== null) &&
Object.values(semanticSearchResultGroups).map((resultGroup) => {
return (
<SearchResultGroup searchResults={resultGroup} collapsedResultCount={3} />
<SearchResultGroup
searchResults={resultGroup}
collapsedResultCount={3}
key={resultGroup?.[0].mediaRecordSegment.mediaRecordId ?? "undefined"} />
);
})
)}
Expand Down
Loading

0 comments on commit 781f4fd

Please sign in to comment.