From d74fe3a25b1cbba5109d76b98d8debc4b50f2716 Mon Sep 17 00:00:00 2001 From: John Rogers Date: Thu, 5 Feb 2026 12:23:03 +0000 Subject: [PATCH 1/6] add coming soon for My Harmony link --- src/components/AppBar.js | 13 +++++++++++++ src/components/HarmonySidebar.js | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/src/components/AppBar.js b/src/components/AppBar.js index 5355cc3..906d8f8 100644 --- a/src/components/AppBar.js +++ b/src/components/AppBar.js @@ -23,6 +23,7 @@ import GoogleIcon from "@mui/icons-material/Google"; import GitHubIcon from "@mui/icons-material/GitHub"; import TwitterIcon from "@mui/icons-material/Twitter"; import { useData } from "../contexts/DataContext"; +import ComingSoonDialog from "./ComingSoonDialog"; const settings = ["My Harmony", "Logout"]; const SettingsIcons = { @@ -34,6 +35,8 @@ function HarmonyAppBar() { const [apiVersion, setApiVersion] = React.useState(null); const [allModels, setAllModels] = React.useState(); const [error, setError] = React.useState(null); + const [comingSoonOpen, setComingSoonOpen] = React.useState(false); + const [comingSoonFeature, setComingSoonFeature] = React.useState(""); const { currentUser, logout, @@ -75,6 +78,11 @@ function HarmonyAppBar() { const handleUserMenuClick = (menuItem) => { switch (menuItem) { + case "My Harmony": + handleCloseUserMenu(); + setComingSoonFeature("My Harmony"); + setComingSoonOpen(true); + break; case "Logout": handleCloseUserMenu(); console.log("logging out"); @@ -267,6 +275,11 @@ function HarmonyAppBar() { + setComingSoonOpen(false)} + featureName={comingSoonFeature} + /> ); } diff --git a/src/components/HarmonySidebar.js b/src/components/HarmonySidebar.js index d106208..78579dd 100644 --- a/src/components/HarmonySidebar.js +++ b/src/components/HarmonySidebar.js @@ -147,6 +147,11 @@ export default function HarmonySidebar() { const handleUserMenuClick = (menuItem) => { switch (menuItem) { + case "My Harmony": + handleCloseUserMenu(); + setComingSoonFeature("My Harmony"); + setComingSoonOpen(true); + break; case "Logout": handleCloseUserMenu(); console.log("logging out"); From e6dd3afda9044f571db958cd4516a4bca336f403 Mon Sep 17 00:00:00 2001 From: John Rogers Date: Thu, 5 Feb 2026 14:16:36 +0000 Subject: [PATCH 2/6] Add Harmony discovery links card and logo to match results --- .cursor-updates | 4 ++ src/components/MatchUnit.js | 84 ++++++++++++++++++++++++++- src/components/Results.js | 113 +++++++++++++++++++++++++++++++++++- 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 .cursor-updates diff --git a/.cursor-updates b/.cursor-updates new file mode 100644 index 0000000..87ba419 --- /dev/null +++ b/.cursor-updates @@ -0,0 +1,4 @@ +# Cursor Updates + +- Added discovery card below Catalog mental health card with Harmony logo and discovery chips for topics and instruments that link to the discovery app +- Added Harmony logo above match percentage in each match results card with tooltip linking to discovery search diff --git a/src/components/MatchUnit.js b/src/components/MatchUnit.js index 44191f3..b37e0df 100644 --- a/src/components/MatchUnit.js +++ b/src/components/MatchUnit.js @@ -7,6 +7,8 @@ import { Menu, MenuItem, ListItemIcon, + Tooltip, + Link, } from "@mui/material"; import "react-circular-progressbar/dist/styles.css"; import MatchCircle from "./MatchCircle"; @@ -19,6 +21,52 @@ const MatchUnit = ({ Q1, Q2, percentage, matchUnitMenuAction, selected }) => { // const [isFlipped, setIsFlipped] = useState(false); const [menuElement, setMenuElement] = useState(null); + // Helper function to get discovery app path + const getDiscoveryNextPath = (path) => { + if (typeof window !== "undefined") { + // Handle local development - DiscoveryNext is on root + if ( + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" + ) { + return path; + } + // Production - DiscoveryNext is under /search + return `/search${path}`; + } + return `/search${path}`; // fallback for SSR + }; + + // Build discovery URL with topics and instruments from this match + const getDiscoveryUrl = () => { + const params = new URLSearchParams(); + + // Add topics if available + const topics = []; + if (Q1.topics_strengths) { + topics.push(...Object.keys(Q1.topics_strengths)); + } + if (Q2.topics_strengths) { + topics.push(...Object.keys(Q2.topics_strengths)); + } + const uniqueTopics = [...new Set(topics)]; + uniqueTopics.forEach(topic => params.append("topics", topic)); + + // Add instruments if available + const instruments = []; + if (Q1.instrument && Q1.instrument.name) { + instruments.push(Q1.instrument.name); + } + if (Q2.instrument && Q2.instrument.name) { + instruments.push(Q2.instrument.name); + } + const uniqueInstruments = [...new Set(instruments)]; + uniqueInstruments.forEach(instrument => params.append("instruments", instrument)); + + const queryString = params.toString(); + return `${getDiscoveryNextPath("/")}${queryString ? `?${queryString}` : ""}`; + }; + const openMenu = (event) => { event.stopPropagation(); setMenuElement(event.currentTarget); @@ -72,7 +120,41 @@ const MatchUnit = ({ Q1, Q2, percentage, matchUnitMenuAction, selected }) => { {Q1.instrument && Q1.instrument.name} - Q{Q1.question_no} - + + + { + e.stopPropagation(); + }} + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + textDecoration: "none", + }} + > + Harmony Logo + + { + if (typeof window !== "undefined") { + // Handle local development - DiscoveryNext is on root + if ( + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" + ) { + return path; // e.g., "/" becomes "/" + } + // Production - DiscoveryNext is under /search + return `/search${path}`; // e.g., "/" becomes "/search/" + } + return `/search${path}`; // fallback for SSR + }; + const getQuestion = useCallback( (qidx) => { return apiData.instruments @@ -241,6 +258,30 @@ export default function Results({ .flat() ), ]); + + // Extract unique instrument names + const instruments = [ + ...new Set( + cm + .map((m) => { + const q = getQuestion(m.qi); + const mq = getQuestion(m.mqi); + let inst = []; + if (q.instrument && q.instrument.name) { + inst.push(q.instrument.name); + } + if (mq.instrument && mq.instrument.name) { + inst.push(mq.instrument.name); + } + return inst; + }) + .flat() + .filter(Boolean) + ), + ]; + setUniqueInstruments(instruments); + } else { + setUniqueInstruments([]); } }, [resultsOptions, apiData, setComputedMatches, getQuestion]); @@ -395,6 +436,76 @@ export default function Results({ )} + {/* Discovery Card - Harmony Discovery Links */} + {(topics.length > 0 || uniqueInstruments.length > 0) && ( + + Harmony Logo + + + Discover: + + + {topics.length > 0 && ( + `topics=${encodeURIComponent(t)}`).join("&")}`} + target="HarmonyDiscovery" + clickable + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} + {uniqueInstruments.length > 0 && ( + `instruments=${encodeURIComponent(i)}`).join("&")}`} + target="HarmonyDiscovery" + clickable + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} + + + + )} {computedMatches && computedMatches.slice(0, 100).map((i) => { return ( From 7868e69039d797a8adfa9557e52f256eb702501b Mon Sep 17 00:00:00 2001 From: John Rogers Date: Thu, 5 Feb 2026 16:18:38 +0000 Subject: [PATCH 3/6] Update discovery card layout and match unit query to use question texts --- .cursor-updates | 4 +- src/components/MatchUnit.js | 27 ++------ src/components/Results.js | 124 +++++++++++++++++++++--------------- 3 files changed, 80 insertions(+), 75 deletions(-) diff --git a/.cursor-updates b/.cursor-updates index 87ba419..af10d4e 100644 --- a/.cursor-updates +++ b/.cursor-updates @@ -1,4 +1,4 @@ # Cursor Updates -- Added discovery card below Catalog mental health card with Harmony logo and discovery chips for topics and instruments that link to the discovery app -- Added Harmony logo above match percentage in each match results card with tooltip linking to discovery search +- Added discovery card below Catalog mental health card with Harmony logo and discovery chips for topics and instruments that link to the discovery app (layout matches Catalog card with logo on one side, content on the other) +- Added Harmony logo above match percentage in each match results card with tooltip linking to discovery search using query parameter with both question texts diff --git a/src/components/MatchUnit.js b/src/components/MatchUnit.js index b37e0df..d15320b 100644 --- a/src/components/MatchUnit.js +++ b/src/components/MatchUnit.js @@ -37,31 +37,18 @@ const MatchUnit = ({ Q1, Q2, percentage, matchUnitMenuAction, selected }) => { return `/search${path}`; // fallback for SSR }; - // Build discovery URL with topics and instruments from this match + // Build discovery URL with both question texts as query const getDiscoveryUrl = () => { const params = new URLSearchParams(); - // Add topics if available - const topics = []; - if (Q1.topics_strengths) { - topics.push(...Object.keys(Q1.topics_strengths)); - } - if (Q2.topics_strengths) { - topics.push(...Object.keys(Q2.topics_strengths)); - } - const uniqueTopics = [...new Set(topics)]; - uniqueTopics.forEach(topic => params.append("topics", topic)); + // Combine both question texts for the query + const queryText = [Q1.question_text, Q2.question_text] + .filter(Boolean) + .join(" "); - // Add instruments if available - const instruments = []; - if (Q1.instrument && Q1.instrument.name) { - instruments.push(Q1.instrument.name); - } - if (Q2.instrument && Q2.instrument.name) { - instruments.push(Q2.instrument.name); + if (queryText) { + params.set("query", queryText); } - const uniqueInstruments = [...new Set(instruments)]; - uniqueInstruments.forEach(instrument => params.append("instruments", instrument)); const queryString = params.toString(); return `${getDiscoveryNextPath("/")}${queryString ? `?${queryString}` : ""}`; diff --git a/src/components/Results.js b/src/components/Results.js index e8b1c67..5a88a09 100644 --- a/src/components/Results.js +++ b/src/components/Results.js @@ -443,67 +443,85 @@ export default function Results({ sx={{ display: "flex", width: "100%", - padding: "1rem", + height: { xs: "10rem", sm: "8rem" }, + padding: "0.5rem", margin: "0 0 1rem 0", + justifyContent: "space-between", alignItems: "center", - gap: 2, + textAlign: "center", }} > - Harmony Logo - - - Discover: - - - {topics.length > 0 && ( - `topics=${encodeURIComponent(t)}`).join("&")}`} - target="HarmonyDiscovery" - clickable - sx={{ - cursor: "pointer", - "&:hover": { - backgroundColor: "action.hover", - }, - }} - onClick={(e) => { - e.stopPropagation(); - }} - /> - )} - {uniqueInstruments.length > 0 && ( - `instruments=${encodeURIComponent(i)}`).join("&")}`} - target="HarmonyDiscovery" - clickable - sx={{ - cursor: "pointer", - "&:hover": { - backgroundColor: "action.hover", - }, - }} - onClick={(e) => { - e.stopPropagation(); - }} - /> - )} + Harmony Logo + + + + Discover: + + + {topics.length > 0 && ( + `topics=${encodeURIComponent(t)}`).join("&")}`} + target="HarmonyDiscovery" + clickable + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} + {uniqueInstruments.length > 0 && ( + `instruments=${encodeURIComponent(i)}`).join("&")}`} + target="HarmonyDiscovery" + clickable + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} + + + Placeholder + - + )} {computedMatches && From 63d67184ebb7759f65c4415d347dcbca85888ed3 Mon Sep 17 00:00:00 2001 From: John Rogers Date: Thu, 5 Feb 2026 16:20:04 +0000 Subject: [PATCH 4/6] Use utility functions for discovery URLs to handle localhost and production correctly --- .cursor-updates | 1 + src/components/MatchUnit.js | 20 ++++++++- src/components/Results.js | 90 ++++++++++++++++++++++--------------- 3 files changed, 72 insertions(+), 39 deletions(-) diff --git a/.cursor-updates b/.cursor-updates index af10d4e..56e7441 100644 --- a/.cursor-updates +++ b/.cursor-updates @@ -2,3 +2,4 @@ - Added discovery card below Catalog mental health card with Harmony logo and discovery chips for topics and instruments that link to the discovery app (layout matches Catalog card with logo on one side, content on the other) - Added Harmony logo above match percentage in each match results card with tooltip linking to discovery search using query parameter with both question texts +- Updated URL generation to use getCurrentDomain() and getDiscoveryNextPath() utility functions for proper localhost (port 3222) and production (/search/) URL handling diff --git a/src/components/MatchUnit.js b/src/components/MatchUnit.js index d15320b..547e759 100644 --- a/src/components/MatchUnit.js +++ b/src/components/MatchUnit.js @@ -21,7 +21,23 @@ const MatchUnit = ({ Q1, Q2, percentage, matchUnitMenuAction, selected }) => { // const [isFlipped, setIsFlipped] = useState(false); const [menuElement, setMenuElement] = useState(null); - // Helper function to get discovery app path + // Get current domain for dynamic links + const getCurrentDomain = () => { + if (typeof window !== "undefined") { + // Handle local development with different ports + if ( + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" + ) { + // React app runs on port 3000, DiscoveryNext runs on port 3222 + return "http://localhost:3222"; + } + return window.location.origin; + } + return "https://harmonydata.ac.uk"; // fallback for SSR + }; + + // Get the correct path for DiscoveryNext links const getDiscoveryNextPath = (path) => { if (typeof window !== "undefined") { // Handle local development - DiscoveryNext is on root @@ -51,7 +67,7 @@ const MatchUnit = ({ Q1, Q2, percentage, matchUnitMenuAction, selected }) => { } const queryString = params.toString(); - return `${getDiscoveryNextPath("/")}${queryString ? `?${queryString}` : ""}`; + return `${getCurrentDomain()}${getDiscoveryNextPath("/")}${queryString ? `?${queryString}` : ""}`; }; const openMenu = (event) => { diff --git a/src/components/Results.js b/src/components/Results.js index 5a88a09..996fc53 100644 --- a/src/components/Results.js +++ b/src/components/Results.js @@ -32,7 +32,23 @@ export default function Results({ const [uniqueInstruments, setUniqueInstruments] = useState([]); ReactGA.send({ hitType: "pageview", page: "/model", title: "Model" }); - // Helper function to get discovery app path + // Get current domain for dynamic links + const getCurrentDomain = () => { + if (typeof window !== "undefined") { + // Handle local development with different ports + if ( + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1" + ) { + // React app runs on port 3000, DiscoveryNext runs on port 3222 + return "http://localhost:3222"; + } + return window.location.origin; + } + return "https://harmonydata.ac.uk"; // fallback for SSR + }; + + // Get the correct path for DiscoveryNext links const getDiscoveryNextPath = (path) => { if (typeof window !== "undefined") { // Handle local development - DiscoveryNext is on root @@ -480,42 +496,42 @@ export default function Results({ Discover: - {topics.length > 0 && ( - `topics=${encodeURIComponent(t)}`).join("&")}`} - target="HarmonyDiscovery" - clickable - sx={{ - cursor: "pointer", - "&:hover": { - backgroundColor: "action.hover", - }, - }} - onClick={(e) => { - e.stopPropagation(); - }} - /> - )} - {uniqueInstruments.length > 0 && ( - `instruments=${encodeURIComponent(i)}`).join("&")}`} - target="HarmonyDiscovery" - clickable - sx={{ - cursor: "pointer", - "&:hover": { - backgroundColor: "action.hover", - }, - }} - onClick={(e) => { - e.stopPropagation(); - }} - /> - )} + {topics.length > 0 && ( + `topics=${encodeURIComponent(t)}`).join("&")}`} + target="HarmonyDiscovery" + clickable + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} + {uniqueInstruments.length > 0 && ( + `instruments=${encodeURIComponent(i)}`).join("&")}`} + target="HarmonyDiscovery" + clickable + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} Placeholder From 764b401482bd83b3abf5de8be372a9128b55b7ea Mon Sep 17 00:00:00 2001 From: John Rogers Date: Fri, 4 Sep 2026 12:30:37 +0100 Subject: [PATCH 5/6] Parse uploaded files one at a time so large PDF batches don't time out A single /text/parse call carrying many large PDFs times out: Tika can take well over a minute per big or scanned PDF, and the client's 15s timeout with three blind retries then re-sent the whole batch while the server was still working on the first attempt. The user saw "Something went wrong" even though each file parses fine on its own. Upload.js now chains one /text/parse call per file, in series, with a progress toast ("Parsing 3 of 15: name.pdf"), shows each file as soon as it lands, and reports which files failed instead of failing the batch. DataContext.parse gets a generous per-file timeout and no retry, since re-sending a PDF that just timed out only piles more work on the server. The API caches parsed files by content, so a re-upload is instant. Fixes #61 Co-Authored-By: Claude Fable 5.1 --- src/components/Upload.js | 80 +++++++++++++++++++++++++------------ src/contexts/DataContext.js | 17 ++++++-- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/components/Upload.js b/src/components/Upload.js index 0a3a1d6..d3a8ba9 100644 --- a/src/components/Upload.js +++ b/src/components/Upload.js @@ -217,34 +217,62 @@ export default function Upload({ ); }); Promise.all(frp) - .then((allFiles) => { - toast.promise( - new Promise((resolve, reject) => { - parse(allFiles) - .then((data) => { - const newFileInfos = [...fileInfos]; - // Load each returned file / instrument in the data - data.forEach((instrument) => { - newFileInfos.push(instrument); - }); - setFileInfos(newFileInfos); - resolve(true); - }) - .catch((e) => { - console.log(e); - setParseError(true); - reject("Parse Error"); - }) - .finally((_) => { - syncFileInfos(); + .then(async (allFiles) => { + // Parse ONE file per API call, in series (issue #61). A single + // /text/parse call carrying many large PDFs times out: Tika can take + // well over a minute per big or scanned PDF, and the old 15s timeout + // with 3 retries then re-sent the whole batch while the server was + // still working on the first attempt. One file per call keeps each + // request inside the proxy limits, shows progress, and lets the good + // files land even if one bad scan fails. The API caches parsed files + // by content, so re-uploading an already-parsed file is instant. + const progress = (i) => + allFiles.length === 1 + ? `Parsing ${allFiles[0].file_name} - this may take a while` + : `Parsing ${i + 1} of ${allFiles.length}: ${allFiles[i].file_name} - this may take a while`; + const toastId = toast.loading(progress(0)); + const newFileInfos = [...fileInfos]; + const failed = []; + let parsedCount = 0; + for (let i = 0; i < allFiles.length; i++) { + toast.update(toastId, { render: progress(i) }); + try { + const data = await parse([allFiles[i]]); + if (Array.isArray(data) && data.length) { + data.forEach((instrument) => { + newFileInfos.push(instrument); }); - }), - { - pending: "Parsing files - this may take a while", - success: "Success!", - error: "Something went wrong - please try again", + // Show each file as soon as it lands rather than at the end + setFileInfos([...newFileInfos]); + parsedCount++; + } else { + failed.push(`${allFiles[i].file_name} (no questions found)`); + } + } catch (e) { + console.log(e); + failed.push(allFiles[i].file_name); } - ); + } + syncFileInfos(); + if (failed.length === 0) { + toast.update(toastId, { + render: "Success!", + type: "success", + isLoading: false, + autoClose: 3000, + }); + } else { + setParseError(true); + toast.update(toastId, { + render: + parsedCount === 0 + ? "Something went wrong - please try again" + : `Parsed ${parsedCount} of ${allFiles.length} files - could not parse ${failed.join(", ")}`, + type: parsedCount === 0 ? "error" : "warning", + isLoading: false, + autoClose: 8000, + }); + } }) .catch((e) => { console.log(e); diff --git a/src/contexts/DataContext.js b/src/contexts/DataContext.js index 65ff6e0..a861be2 100644 --- a/src/contexts/DataContext.js +++ b/src/contexts/DataContext.js @@ -26,9 +26,13 @@ export function DataProvider({ children }) { framework: "huggingface", model: "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2", }); - const retryablePostData = ({ url = "", data = {}, timeout = 8000 }) => { + const retryablePostData = ({ + url = "", + data = {}, + timeout = 8000, + retries = 3, + }) => { return new Promise(async (resolve, reject) => { - var retries = 3; var response; while (retries > 0) { try { @@ -125,11 +129,16 @@ export function DataProvider({ children }) { }); }; - const parse = (allFiles) => { + // Parse is called with ONE file at a time by Upload.js (see issue #61). + // Tika can take well over a minute on a large or scanned PDF, so the + // per-call timeout is generous and there is no blind retry: re-sending a + // big PDF that just timed out only piles more work onto the server. + const parse = (allFiles, timeout = 180000) => { return retryablePostData({ url: process.env.REACT_APP_API_PARSE, data: allFiles, - timeout: 15000, + timeout: timeout, + retries: 1, }); }; const match = useCallback( From 5fb5b985062d859a074591366d37ef84d9179a62 Mon Sep 17 00:00:00 2001 From: John Rogers Date: Thu, 24 Sep 2026 10:43:35 +0100 Subject: [PATCH 6/6] Hide the "Studies using the same instruments" discovery chip Discovery's instruments filter needs its own instrument names; Harmony's names (e.g. "SCARED English (child)", "CES_D English") return zero results, so the chip would land users on an empty search. Kept behind a flag so it can come back once the names are mapped. Co-Authored-By: Claude Opus 5.5 --- src/components/Results.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Results.js b/src/components/Results.js index 996fc53..da34bd4 100644 --- a/src/components/Results.js +++ b/src/components/Results.js @@ -9,6 +9,10 @@ import ResultsOptions from "./ResultsOptions"; import { parse, test } from "liqe"; +// Discovery's instruments filter needs its own instrument names; Harmony's +// (e.g. "SCARED English (child)") match nothing, so hide the chip for now. +const SHOW_INSTRUMENTS_CHIP = false; + export default function Results({ apiData, setApiData, @@ -453,7 +457,8 @@ export default function Results({ )} {/* Discovery Card - Harmony Discovery Links */} - {(topics.length > 0 || uniqueInstruments.length > 0) && ( + {(topics.length > 0 || + (SHOW_INSTRUMENTS_CHIP && uniqueInstruments.length > 0)) && ( )} - {uniqueInstruments.length > 0 && ( + {SHOW_INSTRUMENTS_CHIP && uniqueInstruments.length > 0 && (