Repository: climatepolicyradar/navigator-frontend
Pull request #441: APP-375: Concepts on a document page
Author: login: jesskelsall
Created: 2025-04-02T16:26:24Z | Merged: 2025-04-03T13:43:00Z

# What's changed

Adds the new concept multi-picker to the document page so that passage results can include concepts in filtering. This work largely builds upon @PatFawbertMills's work on the search page and reuses as many existing components as possible.

- `ConceptsDocumentViewer` (document page):
  - Rearranged sidebar and fiddled with a lot of padding to make everything flow better.
  - Added syntactic sugar logic variables so the JSX reads easier.
  - Added `SideCol` to the left of the document viewer.
    - Uses `ConceptPicker` to provide concept filtering options.
    - Show/hide button for `ConceptPicker` when in single column layout.
- `ConceptPicker`
  - Moved title into a prop so each use can have its own.
  - Added `containerClasses` for further customisation.

**Includes #437 changes, so don't review this until it is merged!**

## Why?

Implements the following tickets (which are all essentially the same):
- APP-375 Concepts on a document page
- APP-292 Multiple concept search on document page
- APP-388 Document page utilising search in header and concept picker

## Screenshots?

Wide, no concepts selected:
<img width="1773" alt="Screenshot 2025-04-02 at 17 16 57" src="https://github.com/user-attachments/assets/f6c4ea5c-73ef-4553-b169-05afef8f5d90" />

Wide, one concept selected:
<img width="1770" alt="Screenshot 2025-04-02 at 17 17 12" src="https://github.com/user-attachments/assets/3e0d2857-1594-42b7-964f-aecb7726bdd6" />

Narrow, concepts hidden:
<img width="619" alt="Screenshot 2025-04-02 at 17 17 31" src="https://github.com/user-attachments/assets/238bfd65-7ad2-456a-b3b2-fc2b09ce0530" />

Narrow, concepts visible:
<img width="616" alt="Screenshot 2025-04-02 at 17 17 39" src="https://github.com/user-attachments/assets/89e33dcd-802b-47bc-8532-8018974fbfd9" />


Diff:
diff --git a/src/components/documents/ConceptsDocumentViewer.tsx b/src/components/documents/ConceptsDocumentViewer.tsx
index 6c292e04f..9c57f0001 100644
--- a/src/components/documents/ConceptsDocumentViewer.tsx
+++ b/src/components/documents/ConceptsDocumentViewer.tsx
@@ -1,23 +1,24 @@
-import { TConcept, TDocumentPage, TSearchResponse } from "@/types";
-import EmbeddedPDF from "@/components/EmbeddedPDF";
-import { FullWidth } from "@/components/panels/FullWidth";
-import { EmptyDocument } from "./EmptyDocument";
-import { LuSettings2 } from "react-icons/lu";
-import { AnimatePresence } from "framer-motion";
 import { UnavailableConcepts } from "@/components/documents/UnavailableConcepts";
+import EmbeddedPDF from "@/components/EmbeddedPDF";
+import { SearchSettings } from "@/components/filters/SearchSettings";
+import Loader from "@/components/Loader";
 import PassageMatches from "@/components/PassageMatches";
 import { SearchLimitTooltip } from "@/components/tooltip/SearchLimitTooltip";
-import { EmptyPassages } from "./EmptyPassages";
-import { motion } from "framer-motion";
-import { useEffect, useState, useCallback, useMemo, useReducer } from "react";
+import { MAX_PASSAGES, MAX_RESULTS } from "@/constants/paging";
+import { QUERY_PARAMS } from "@/constants/queryParams";
+import { useEffectOnce } from "@/hooks/useEffectOnce";
 import useSearch from "@/hooks/useSearch";
+import { TConcept, TDocumentPage, TSearchResponse } from "@/types";
 import { fetchAndProcessConcepts } from "@/utils/processConcepts";
-import { useEffectOnce } from "@/hooks/useEffectOnce";
-import { QUERY_PARAMS } from "@/constants/queryParams";
-import { MAX_PASSAGES, MAX_RESULTS } from "@/constants/paging";
-import { SearchSettings } from "@/components/filters/SearchSettings";
-import { ConceptsPanel } from "@/components/concepts/ConceptsPanel";
-import Loader from "@/components/Loader";
+import { AnimatePresence, motion } from "framer-motion";
+import React, { useCallback, useEffect, useMemo, useReducer, useState } from "react";
+import { LuSettings2 } from "react-icons/lu";
+import { Button } from "../atoms/button/Button";
+import { Icon } from "../atoms/icon/Icon";
+import { ConceptPicker } from "../organisms/ConceptPicker";
+import { SideCol } from "../panels/SideCol";
+import { EmptyDocument } from "./EmptyDocument";
+import { EmptyPassages } from "./EmptyPassages";
 
 type TProps = {
   initialQueryTerm?: string | string[];
@@ -30,14 +31,13 @@ type TProps = {
   familySlug: string;
   // Callback props for state changes
   onExactMatchChange?: (isExact: boolean) => void;
-  onConceptClick?: (conceptLabel: string) => void;
 };
 
 const passageClasses = (docType: string) => {
   if (docType === "application/pdf") {
-    return "md:w-1/3";
+    return "xl:w-1/3";
   }
-  return "md:w-2/3";
+  return "xl:w-2/3";
 };
 
 const renderPassageCount = (count: number): string => {
@@ -54,9 +54,9 @@ export const ConceptsDocumentViewer = ({
   vespaFamilyData,
   vespaDocumentData,
   onExactMatchChange,
-  onConceptClick,
 }: TProps) => {
   const [showSearchOptions, setShowSearchOptions] = useState(false);
+  const [showConcepts, setShowConcepts] = useState(false);
 
   const getQueryTerm = (term: string | string[]) => (Array.isArray(term) ? term[0] : term || "");
 
@@ -74,7 +74,6 @@ export const ConceptsDocumentViewer = ({
     setState({ queryTerm: newQueryTerm });
   }, [initialQueryTerm]);
 
-  const [rootConcepts, setRootConcepts] = useState<TConcept[]>([]);
   const [familyConcepts, setFamilyConcepts] = useState<TConcept[]>([]);
 
   const canPreview = document.content_type === "application/pdf";
@@ -91,13 +90,12 @@ export const ConceptsDocumentViewer = ({
       });
     });
 
-    fetchAndProcessConcepts(Array.from(conceptIds)).then(({ rootConcepts, concepts }) => {
-      setRootConcepts(rootConcepts);
+    fetchAndProcessConcepts(Array.from(conceptIds)).then(({ concepts }) => {
       setFamilyConcepts(concepts);
     });
   });
 
-  const documentConcepts = useMemo(() => {
+  const documentConcepts: TConcept[] = useMemo(() => {
     const uniqueConceptMap = new Map<string, { concept: TConcept; count: number }>();
 
     (vespaDocumentData?.families ?? []).forEach((family) => {
@@ -127,18 +125,6 @@ export const ConceptsDocumentViewer = ({
       .sort((a, b) => (b.count || 0) - (a.count || 0));
   }, [vespaDocumentData, familyConcepts]);
 
-  const documentConceptCountsById = useMemo(
-    () =>
-      documentConcepts.reduce(
-        (acc, concept) => {
-          acc[concept.wikibase_id] = concept.count || 0;
-          return acc;
-        },
-        {} as Record<string, number>
-      ),
-    [documentConcepts]
-  );
-
   const selectedConcepts = useMemo(
     () =>
       initialConceptFilters
@@ -213,149 +199,157 @@ export const ConceptsDocumentViewer = ({
     [onExactMatchChange]
   );
 
+  const handleToggleConcepts = () => {
+    setShowConcepts((current) => !current);
+  };
+
+  if (!documentConcepts.length) return;
+
+  const isLoading = status !== "success";
+  const hasConcepts = selectedConcepts.length > 0;
+  const showPassages = initialQueryTerm !== "" && state.totalNoOfMatches > 0;
+  const hasNoPassages = !showPassages || (state.totalNoOfMatches === 0 && unavailableConcepts.length === 0);
+  const hasUnavailableConcepts = state.totalNoOfMatches === 0 && unavailableConcepts.length > 0;
+
   return (
-    <>
-      {documentConcepts.length > 0 && (
-        <section className="flex-1 flex" id="document-concepts-viewer">
-          <FullWidth extraClasses="flex-1">
-            <div id="document-container" className="flex flex-col md:flex-row md:h-[90vh]">
-              <div id="document-preview" className={`pt-4 flex-1 h-[400px] basis-[400px] md:block md:h-full md:border-r md:border-r-gray-200`}>
-                {canPreview && (
-                  <EmbeddedPDF
-                    document={document}
-                    documentPassageMatches={state.passageMatches}
-                    passageIndex={state.passageIndex}
-                    startingPassageIndex={initialPassage}
-                  />
-                )}
-                {!canPreview && <EmptyDocument />}
+    <section className="flex-1 xl:px-5" id="document-concepts-viewer">
+      <div id="document-container" className="flex flex-col xl:flex-row xl:h-[90vh]">
+        {/* Concepts */}
+        <SideCol id="document-concepts" extraClasses="!w-full xl:!w-maxSidebar">
+          <div className="p-4 xl:hidden">
+            <Button content="both" onClick={handleToggleConcepts}>
+              <span>{showConcepts ? "Hide" : "Show"} concepts</span>
+              <div className={showConcepts ? "rotate-180" : ""}>
+                <Icon name="downChevron" />
               </div>
-              <div
-                id="document-sidebar"
-                className={`flex flex-col overflow-y-auto py-4 order-first max-h-[90vh] scrollbar-thumb-gray-200 scrollbar-thin scrollbar-track-white scrollbar-thumb-rounded-full hover:scrollbar-thumb-gray-500 md:order-last md:max-h-full md:max-w-[480px] md:min-w-[400px] md:grow-0 md:shrink-0 ${passageClasses(
-                  document.content_type
-                )}`}
-              >
-                <div id="document-search" className="flex flex-col gap-2 md:pl-4">
-                  {unavailableConcepts.length === 0 && (
-                    <div className="relative z-10 flex justify-end">
-                      <button
-                        className="px-4 flex justify-center items-center text-textDark text-xl"
-                        onClick={() => setShowSearchOptions(!showSearchOptions)}
-                      >
-                        <LuSettings2 />
-                      </button>
-                      <AnimatePresence initial={false}>
-                        {showSearchOptions && (
-                          <motion.div
-                            key="content"
-                            initial="collapsed"
-                            animate="open"
-                            exit="collapsed"
-                            variants={{
-                              collapsed: { opacity: 0, transition: { duration: 0.1 } },
-                              open: { opacity: 1, transition: { duration: 0.25 } },
-                            }}
-                          >
-                            <SearchSettings
-                              queryParams={searchQueryParams}
-                              handleSearchChange={handleSemanticSearchChange}
-                              setShowOptions={setShowSearchOptions}
-                            />
-                          </motion.div>
-                        )}
-                      </AnimatePresence>
-                    </div>
-                  )}
-
-                  {selectedConcepts.length === 0 && !initialQueryTerm && (
-                    <ConceptsPanel
-                      rootConcepts={rootConcepts}
-                      concepts={documentConcepts}
-                      conceptCountsById={documentConceptCountsById}
-                      onConceptClick={onConceptClick}
-                      showCounts={false}
-                    ></ConceptsPanel>
-                  )}
-
-                  {selectedConcepts.length > 0 && (
-                    <div className="pt-6 pb-6">
-                      <p className="mb-2 capitalize text-[15px] font-medium text-neutral-800 text-base leading-normal flex-grow">
-                        {selectedConcepts.map((concept) => concept.preferred_label).join(", ")}
+            </Button>
+          </div>
+          <ConceptPicker
+            concepts={documentConcepts}
+            showSearch={false}
+            title={<p className="text-base font-medium">In this document</p>}
+            containerClasses={`pt-4 pr-4 pl-4 xl:pl-0 ${showConcepts ? "" : "hidden xl:flex"}`}
+          />
+        </SideCol>
+
+        {/* Preview */}
+        <div
+          id="document-preview"
+          className={`flex-1 order-last xl:order-none h-[400px] basis-[400px] xl:block xl:h-full xl:border-x xl:border-x-gray-200 px-4 xl:px-0`}
+        >
+          {canPreview && (
+            <EmbeddedPDF
+              document={document}
+              documentPassageMatches={state.passageMatches}
+              passageIndex={state.passageIndex}
+              startingPassageIndex={initialPassage}
+            />
+          )}
+          {!canPreview && <EmptyDocument />}
+        </div>
+
+        {/* Sidebar */}
+        <div
+          id="document-sidebar"
+          className={`flex flex-col overflow-y-auto max-h-[90vh] mr-4 xl:mr-0 scrollbar-thumb-gray-200 scrollbar-thin scrollbar-track-white scrollbar-thumb-rounded-full hover:scrollbar-thumb-gray-500 xl:max-h-full xl:max-w-[480px] xl:min-w-[400px] xl:grow-0 xl:shrink-0 ${passageClasses(
+            document.content_type
+          )}`}
+        >
+          <div className="flex justify-between p-4">
+            <h1 className="text-base font-medium">Passage matches</h1>
+            <button className="text-xl text-text-tertiary" onClick={() => setShowSearchOptions(!showSearchOptions)}>
+              <LuSettings2 />
+            </button>
+            <AnimatePresence initial={false}>
+              {showSearchOptions && (
+                <motion.div
+                  key="content"
+                  initial="collapsed"
+                  animate="open"
+                  exit="collapsed"
+                  variants={{
+                    collapsed: { opacity: 0, transition: { duration: 0.1 } },
+                    open: { opacity: 1, transition: { duration: 0.25 } },
+                  }}
+                >
+                  <SearchSettings
+                    queryParams={searchQueryParams}
+                    handleSearchChange={handleSemanticSearchChange}
+                    setShowOptions={setShowSearchOptions}
+                  />
+                </motion.div>
+              )}
+            </AnimatePresence>
+          </div>
+
+          {isLoading ? (
+            <div className="w-full flex justify-center flex-1">
+              <Loader />
+            </div>
+          ) : (
+            <>
+              {hasConcepts && !hasNoPassages && (
+                <div className="px-4">
+                  {selectedConcepts.map((concept) => (
+                    <React.Fragment key={concept.wikibase_id}>
+                      <p className="mt-4 my-2 capitalize text-[15px] font-medium text-neutral-800 text-base leading-normal flex-grow">
+                        {concept.preferred_label}
                       </p>
-                      {selectedConcepts.map((concept) => (
-                        <p key={concept.wikibase_id} className="pt-1 pb-1">
-                          {concept.description}
-                        </p>
-                      ))}
-                    </div>
-                  )}
+                      <p className="mt-2 my-4">{concept.description}</p>
+                    </React.Fragment>
+                  ))}
                 </div>
-
-                {status !== "success" ? (
-                  <div className="w-full flex justify-center flex-1 bg-white">
-                    <Loader />
+              )}
+
+              {showPassages && (
+                <>
+                  <div className="border-gray-200 p-4 text-sm border-b xl:pl-4" data-cy="document-matches-description">
+                    <div className="mb-2">
+                      Displaying {renderPassageCount(state.totalNoOfMatches)}{" "}
+                      {initialQueryTerm && (
+                        <>
+                          for "<span className="text-textDark font-medium">{`${initialQueryTerm}`}</span>"
+                        </>
+                      )}
+                      {initialQueryTerm && !searchQuery.exact_match && ` and related phrases`}
+                      {selectedConcepts.length > 0 && (
+                        <>
+                          {" in "}
+                          <b>{selectedConcepts.map((concept) => concept.preferred_label).join(", ")}</b>
+                        </>
+                      )}
+                      {state.totalNoOfMatches >= MAX_RESULTS && (
+                        <span className="ml-1 inline-block">
+                          <SearchLimitTooltip colour="grey" />
+                        </span>
+                      )}
+                    </div>
+                    <p>Sorted by search relevance</p>
                   </div>
-                ) : (
-                  <>
-                    {initialQueryTerm !== "" && state.totalNoOfMatches > 0 && (
-                      <>
-                        <div className="border-gray-200 my-4 text-sm pb-4 border-b md:pl-4" data-cy="document-matches-description">
-                          <div className="mb-2">
-                            Displaying {renderPassageCount(state.totalNoOfMatches)}{" "}
-                            {initialQueryTerm && (
-                              <>
-                                for "<span className="text-textDark font-medium">{`${initialQueryTerm}`}</span>"
-                              </>
-                            )}
-                            {initialQueryTerm && !searchQuery.exact_match && ` and related phrases`}
-                            {selectedConcepts.length > 0 && (
-                              <>
-                                {" in "}
-                                <b>{selectedConcepts.map((concept) => concept.preferred_label).join(", ")}</b>
-                              </>
-                            )}
-                            {state.totalNoOfMatches >= MAX_RESULTS && (
-                              <span className="ml-1 inline-block">
-                                <SearchLimitTooltip colour="grey" />
-                              </span>
-                            )}
-                          </div>
-                          <p>Sorted by search relevance</p>
-                        </div>
-                        <div
-                          id="document-passage-matches"
-                          className="relative overflow-y-scroll scrollbar-thumb-gray-200 scrollbar-thin scrollbar-track-white scrollbar-thumb-rounded-full hover:scrollbar-thumb-gray-500 md:pl-4"
-                        >
-                          <PassageMatches
-                            passages={state.passageMatches}
-                            onClick={handlePassageClick}
-                            activeIndex={state.passageIndex ?? initialPassage}
-                          />
-                        </div>
-                      </>
-                    )}
-
-                    {state.totalNoOfMatches === 0 && unavailableConcepts.length === 0 && (
-                      <EmptyPassages
-                        hasQueryString={
-                          !!searchQueryParams[QUERY_PARAMS.query_string] &&
-                          !!searchQueryParams[QUERY_PARAMS.concept_id] &&
-                          !!searchQueryParams[QUERY_PARAMS.concept_name]
-                        }
-                      />
-                    )}
-
-                    {state.totalNoOfMatches === 0 && unavailableConcepts.length > 0 && (
-                      <UnavailableConcepts unavailableConcepts={unavailableConcepts} familySlug={familySlug} />
-                    )}
-                  </>
-                )}
-              </div>
-            </div>
-          </FullWidth>
-        </section>
-      )}
-    </>
+                  <div
+                    id="document-passage-matches"
+                    className="relative xl:overflow-y-scroll scrollbar-thumb-gray-200 scrollbar-thin scrollbar-track-white scrollbar-thumb-rounded-full hover:scrollbar-thumb-gray-500 px-4"
+                  >
+                    <PassageMatches passages={state.passageMatches} onClick={handlePassageClick} activeIndex={state.passageIndex ?? initialPassage} />
+                  </div>
+                </>
+              )}
+
+              {hasNoPassages && (
+                <EmptyPassages
+                  hasQueryString={
+                    !!searchQueryParams[QUERY_PARAMS.query_string] &&
+                    !!searchQueryParams[QUERY_PARAMS.concept_id] &&
+                    !!searchQueryParams[QUERY_PARAMS.concept_name]
+                  }
+                />
+              )}
+
+              {hasUnavailableConcepts && <UnavailableConcepts unavailableConcepts={unavailableConcepts} familySlug={familySlug} />}
+            </>
+          )}
+        </div>
+      </div>
+    </section>
   );
 };
diff --git a/src/components/documents/UnavailableConcepts.tsx b/src/components/documents/UnavailableConcepts.tsx
index eb6c3ead6..00455168f 100644
--- a/src/components/documents/UnavailableConcepts.tsx
+++ b/src/components/documents/UnavailableConcepts.tsx
@@ -14,7 +14,6 @@ export const UnavailableConcepts = ({ unavailableConcepts, familySlug }: TProps)
       </div>
     </div>
     <p className="text-xl font-medium">Concept not in Document</p>
-    <p>Concept matching is currently in beta mode.</p>
     <p>
       Some concepts from your search are not be present in this document but might be present in{" "}
       <Link
@@ -26,7 +25,7 @@ export const UnavailableConcepts = ({ unavailableConcepts, familySlug }: TProps)
       .
     </p>
     <p>
-      The following concept {unavailableConcepts.length > 1 ? "s are" : " is"} not present in this document:
+      The following {unavailableConcepts.length > 1 ? "concepts are" : "concept is"} not present in this document:
       <p className="font-medium">{unavailableConcepts.join(", ")}</p>
     </p>
   </div>
diff --git a/src/components/organisms/ConceptPicker.tsx b/src/components/organisms/ConceptPicker.tsx
index 872e825ed..639afef51 100644
--- a/src/components/organisms/ConceptPicker.tsx
+++ b/src/components/organisms/ConceptPicker.tsx
@@ -1,7 +1,6 @@
 import { useEffect, useRef, useState } from "react";
 import { NextRouter, useRouter } from "next/router";
 
-import { Label } from "@/components/labels/Label";
 import { InputCheck } from "@/components/forms/Checkbox";
 import { Select } from "@/components/atoms/select/Select";
 
@@ -14,7 +13,10 @@ import { TConcept } from "@/types";
 
 type TProps = {
   concepts: TConcept[];
+  containerClasses?: string;
+  showSearch?: boolean;
   startingSort?: TSort;
+  title: React.ReactNode;
 };
 
 const SORT_OPTIONS = ["A-Z", "Grouped"] as const;
@@ -64,20 +66,7 @@ const onConceptChange = (router: NextRouter, concept: TConcept) => {
   router.push({ query: query }, undefined, { shallow: true });
 };
 
-const getSelectedConcepts = (concepts: TConcept[], router: NextRouter) => {
-  return concepts.reduce((acc, concept) => {
-    const queryValue = router.query[QUERY_PARAMS.concept_name];
-    if (!queryValue) {
-      return acc;
-    }
-    if (typeof queryValue === "string") {
-      return queryValue === concept.preferred_label ? [...acc, concept] : acc;
-    }
-    return queryValue.includes(concept.preferred_label) ? [...acc, concept] : acc;
-  }, [] as TConcept[]);
-};
-
-export const ConceptPicker = ({ concepts, startingSort = "A-Z" }: TProps) => {
+export const ConceptPicker = ({ concepts, containerClasses = "", startingSort = "A-Z", showSearch = true, title }: TProps) => {
   const router = useRouter();
   const ref = useRef(null);
   const [search, setSearch] = useState("");
@@ -101,12 +90,9 @@ export const ConceptPicker = ({ concepts, startingSort = "A-Z" }: TProps) => {
   }, [concepts]);
 
   return (
-    <div className="relative flex flex-col gap-5 max-h-full pb-5" ref={ref}>
+    <div className={`relative flex flex-col gap-5 max-h-full pb-5 ${containerClasses}`} ref={ref}>
       <div className="flex items-center justify-between">
-        <div className="flex items-center gap-2">
-          <div className="text-[15px] font-medium text-text-primary">Concepts</div>
-          <Label>Beta</Label>
-        </div>
+        {title}
         <div className="basis-1/3">
           <Select
             defaultValue="A-Z"
@@ -119,7 +105,7 @@ export const ConceptPicker = ({ concepts, startingSort = "A-Z" }: TProps) => {
       </div>
       {/* SCROLL AREA */}
       <div className="flex-1 flex flex-col gap-5 overflow-y-scroll scrollbar-thumb-scrollbar scrollbar-thin scrollbar-track-white scrollbar-thumb-rounded-full hover:scrollbar-thumb-scrollbar-darker">
-        <input type="text" placeholder="Quick search" value={search} onChange={(e) => setSearch(e.target.value)} />
+        {showSearch && <input type="text" placeholder="Quick search" value={search} onChange={(e) => setSearch(e.target.value)} />}
         <div className="flex flex-col gap-2 text-sm">
           {/* GROUPED SORT */}
           {sort === "Grouped" &&
diff --git a/src/components/panels/SideCol.tsx b/src/components/panels/SideCol.tsx
index 9ecc14296..29a3b2f02 100644
--- a/src/components/panels/SideCol.tsx
+++ b/src/components/panels/SideCol.tsx
@@ -1,9 +1,8 @@
 type TProps = {
   extraClasses?: string;
-  children?: React.ReactNode;
 };
 
-export function SideCol({ extraClasses = "", children, ...props }: TProps) {
+export function SideCol({ extraClasses = "", children, ...props }: TProps & React.HTMLAttributes<HTMLDivElement>) {
   return (
     <div className={`w-maxSidebar grow-0 shrink-0 ${extraClasses}`} {...props}>
       {children}
diff --git a/src/pages/documents/[id].tsx b/src/pages/documents/[id].tsx
index f3cc8c217..8d733fc3e 100644
--- a/src/pages/documents/[id].tsx
+++ b/src/pages/documents/[id].tsx
@@ -24,7 +24,7 @@ import { QUERY_PARAMS } from "@/constants/queryParams";
 import { getDocumentDescription } from "@/constants/metaDescriptions";
 import { MAX_PASSAGES, MAX_RESULTS } from "@/constants/paging";
 
-import { TDocumentPage, TFamilyPage, TPassage, TTheme, TSearchResponse } from "@/types";
+import { TDocumentPage, TFamilyPage, TPassage, TTheme, TSearchResponse, TConcept } from "@/types";
 import { getFeatureFlags } from "@/utils/featureFlags";
 import { ConceptsDocumentViewer } from "@/components/documents/ConceptsDocumentViewer";
 import { getMatchedPassagesFromSearch } from "@/utils/getMatchedPassagesFromFamiy";
@@ -169,44 +169,6 @@ const DocumentPage: InferGetServerSidePropsType<typeof getServerSideProps> = ({
     [conceptFiltersQuery]
   );
 
-  const handleConceptClick = useCallback(
-    (conceptLabel: string) => {
-      setPassageIndex(0);
-      if (conceptLabel === "") return false;
-
-      const currentConceptFilters = conceptFilters || [];
-      const queryObj = { ...router.query };
-
-      let updatedConceptFilters;
-
-      // If the concept is already in filters, remove it
-      if (currentConceptFilters.includes(conceptLabel)) {
-        updatedConceptFilters = currentConceptFilters.filter((concept) => concept !== conceptLabel);
-      } else {
-        // If the concept is not in filters, add it
-        updatedConceptFilters = [...currentConceptFilters, conceptLabel];
-      }
-
-      // If no concept filters remain, remove the concept_name query param entirely
-      if (updatedConceptFilters.length === 0) {
-        delete queryObj[QUERY_PARAMS.concept_name];
-      } else {
-        // Otherwise, update the concept filters
-        queryObj[QUERY_PARAMS.concept_name] = updatedConceptFilters;
-      }
-
-      router.push(
-        {
-          pathname: `/documents/${document.slug}`,
-          query: queryObj,
-        },
-        undefined,
-        { shallow: true }
-      );
-    },
-    [router, document.slug, conceptFilters]
-  );
-
   return (
     <Layout title={`${document.title}`} description={getDocumentDescription(document.title)} theme={theme}>
       <section
@@ -326,7 +288,6 @@ const DocumentPage: InferGetServerSidePropsType<typeof getServerSideProps> = ({
             familySlug={family.slug}
             document={document}
             onExactMatchChange={handleExactMatchChange}
-            onConceptClick={handleConceptClick}
           />
         )}
       </section>
diff --git a/src/pages/search/index.tsx b/src/pages/search/index.tsx
index 8f6f2fd07..d40a59b8a 100644
--- a/src/pages/search/index.tsx
+++ b/src/pages/search/index.tsx
@@ -32,6 +32,7 @@ import { Icon } from "@/components/atoms/icon/Icon";
 import { Button } from "@/components/atoms/button/Button";
 import { ConceptPicker } from "@/components/organisms/ConceptPicker";
 import { SlideOut } from "@/components/atoms/SlideOut/SlideOut";
+import { Label } from "@/components/labels/Label";
 
 import { getThemeConfigLink } from "@/utils/getThemeConfigLink";
 import { readConfigFile } from "@/utils/readConfigFile";
@@ -446,7 +447,15 @@ const Search: InferGetServerSidePropsType<typeof getServerSideProps> = ({ theme,
                       />
                     </div>
                     <SlideOut showCloseButton={false}>
-                      <ConceptPicker concepts={conceptsData} />
+                      <ConceptPicker
+                        concepts={conceptsData}
+                        title={
+                          <div className="flex items-center gap-2">
+                            <div className="text-[15px] font-medium text-text-primary">Concepts</div>
+                            <Label>Beta</Label>
+                          </div>
+                        }
+                      />
                     </SlideOut>
                   </div>
                 </>
diff --git a/src/types/types.ts b/src/types/types.ts
index 1cdd16365..205f77d68 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -319,17 +319,18 @@ export type TOrganisationDictionary = TDictionary<TOrganisation>;
 export type TCorpusTypeDictionary = TDictionary<TCorpusType>;
 
 export type TConcept = {
-  preferred_label: string;
-  wikibase_id: string;
   alternative_labels: string[];
-  negative_labels: string[];
+  count?: number;
+  definition?: string;
   description: string;
-  subconcept_of: string[];
-  recursive_subconcept_of: string[];
   has_subconcept: string[];
-  related_concepts: string[];
-  definition?: string;
   labelled_passages?: [];
+  negative_labels: string[];
+  preferred_label: string;
+  recursive_subconcept_of: string[];
+  related_concepts: string[];
+  subconcept_of: string[];
+  wikibase_id: string;
 };
 
 export type TSearchResponse = {


Reviews:
user: login: jesskelsall
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T09:02:27Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T09:45:20Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T09:45:37Z
user: login: PatFawbertMills
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T09:53:16Z
user: login: PatFawbertMills
body: Good work @jesskelsall 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T12:56:26Z

Review comments:
diff_hunk: @@ -64,20 +66,7 @@ const onConceptChange = (router: NextRouter, concept: TConcept) => {
   router.push({ query: query }, undefined, { shallow: true });
 };
 
-const getSelectedConcepts = (concepts: TConcept[], router: NextRouter) => {
path: src/components/organisms/ConceptPicker.tsx
user: login: jesskelsall
body: Removed as this function wasn't being called anywhere.
created_at: 2025-04-03T09:02:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -64,20 +66,7 @@ const onConceptChange = (router: NextRouter, concept: TConcept) => {
   router.push({ query: query }, undefined, { shallow: true });
 };
 
-const getSelectedConcepts = (concepts: TConcept[], router: NextRouter) => {
path: src/components/organisms/ConceptPicker.tsx
user: login: PatFawbertMills
body: Yeh I left it in case we ever wanted to use the logic of showing the selected concepts first. Conor pointed out that we will already be showing applied filters in two other places so probably not needed.
created_at: 2025-04-03T09:45:20Z
author_association: COLLABORATOR
diff_hunk: @@ -64,20 +66,7 @@ const onConceptChange = (router: NextRouter, concept: TConcept) => {
   router.push({ query: query }, undefined, { shallow: true });
 };
 
-const getSelectedConcepts = (concepts: TConcept[], router: NextRouter) => {
path: src/components/organisms/ConceptPicker.tsx
user: login: PatFawbertMills
body: Hoarder-mentality 😆 
created_at: 2025-04-03T09:45:37Z
author_association: COLLABORATOR
path: src/components/documents/ConceptsDocumentViewer.tsx
user: login: PatFawbertMills
body: Question/reservatio: if policy signed off on the concept work tomorrow, would we be confident switching over to using this component in replacement for the current document viewer?

I need to dig into it a little more but I am slightly concerned we have repeated and or overlapping functionality between the page view component and this viewer component - if that makes sense?

There are also a few approaches in this file that I am not totally happy with, such as the `useEffectOnce` workaround, that to me, may indicate a bigger issue.

I can appreciate you didn't create this file and are merely updating it.
created_at: 2025-04-03T09:53:15Z
author_association: COLLABORATOR

PR comments:
user: login: linear[bot]
created_at: 2025-04-02T16:26:27Z
body: <p><a href="https://linear.app/climate-policy-radar/issue/APP-375/concepts-on-a-document-page">APP-375 Concepts on a document page</a></p>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-02T16:29:20Z
body: ## size-limit report 📦 
| Path              | Size                  |
| ----------------- | --------------------- |
| Next static files | 363.81 KB (+0.07% 🔺) |
author_association: NONE
user: login: jesskelsall
created_at: 2025-04-03T13:30:00Z
body: Quick note: @PatFawbertMills has approved this on the understanding that there will be some iteration early next week, but for now the main priority is having something that the policy team can test.
author_association: CONTRIBUTOR
user: login: trunk-io[bot]
created_at: 2025-04-03T13:34:51Z
body: <!-- Trunk Metrics Report -->



⏱️ 23m total CI duration on this PR

| Job | Cumulative Duration | Recent Runs |
|---|---|---|
| [test-e2e](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921809611) | 10m | [🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14239102429/job/39904678132)&#8201;[🟥](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921204337)&#8201;[🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921809611)&#8201; |
| [size](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921202846) | 5m | [🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14239102429/job/39904676841)&#8201;[🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921202846)&#8201; |
| [percy](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921203865) | 3m | [🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14239102429/job/39904677337)&#8201;[🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921203865)&#8201; |
| [test](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921202332) | 3m | [🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14239102429/job/39904676431)&#8201;[🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921202332)&#8201; |
| [code-quality](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921203435) | 2m | [🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14239102429/job/39904677707)&#8201;[🟩](https://github.com/climatepolicyradar/navigator-frontend/actions/runs/14244282291/job/39921203435)&#8201; |

<sub>[settings](https://app.trunk.io/climatepolicyradar/settings/repo?repo=climatepolicyradar/navigator-frontend&utm_source=gh-comment#ci-analytics) ⋅ [feedback](https://form.typeform.com/to/w7Iv34u2?utm_source=gh-comment) ⋅ [docs](https://docs.trunk.io/ci-analytics/pull-request-metrics?utm_source=gh-comment) ⋅ learn more about [trunk.io](https://trunk.io?utm_source=gh-comment)</sub>
author_association: NONE
user: login: trunk-io[bot]
created_at: 2025-04-03T14:38:23Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/33_tests-cc846aa9-purple)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/441?repo=climatepolicyradar/navigator-frontend&commitHash=cc846aa9d5636da50f2c7510fc801bf7064ab2cf&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/1-failed-crimson)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/441?repo=climatepolicyradar/navigator-frontend&commitHash=cc846aa9d5636da50f2c7510fc801bf7064ab2cf&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-flaky-red)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/441?repo=climatepolicyradar/navigator-frontend&commitHash=cc846aa9d5636da50f2c7510fc801bf7064ab2cf&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/441?repo=climatepolicyradar/navigator-frontend&commitHash=cc846aa9d5636da50f2c7510fc801bf7064ab2cf&utm_source=gh-comment&runConclusion=QUARANTINED)


| Failed Test | Failure Summary | Logs |
| --- | --- | --- |
| <code>search</code> |  | [Logs ↗︎](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/441?repo=climatepolicyradar/navigator-frontend&commitHash=cc846aa9d5636da50f2c7510fc801bf7064ab2cf&utm_source=gh-comment&runConclusion=FAILURE&runId=2da03610-08aa-527c-ab1b-0170bc1ba08c) |
<sub>

[View Full Report ↗︎](https://app.trunk.io/climatepolicyradar/flaky-tests/pr/441?repo=climatepolicyradar/navigator-frontend&commitHash=cc846aa9d5636da50f2c7510fc801bf7064ab2cf&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE