Repository navigation
Enhance results bar, context menu, and citation animations - #5
Conversation
- Results bar now shows three states based on what's in the graph: none → "Add all to graph", mixed → "Add N to graph" + "Remove N", all → "Remove all from graph"; individual badges still work - Added removeNodes() for batch removal without N re-renders - Draft citation edge now tracks source node position each tick, so the anchor doesn't freeze when the simulation moves the node Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rrow offset and Seed/All filter tabs - Arrow head: new #arrow-draft marker (refX=10) so the arrowhead tip tracks the cursor instead of lagging ~1cm behind - Double-click edge-draw: dblclick a node enters persistent draw mode; click any other node to create the citation, click background or press Esc to cancel; complements the existing hold-to-drag flow - Help modal: ? button in header opens a modal explaining navigation, adding papers, drawing arrows, and exploring citations - Right-click context menu: contextmenu on a node shows "Cited by this", "Citing this", and "Remove from graph"; closes on outside click or Esc - Seed filter fix: track allSeedPapers[] (populated on Load); Seed tab always shows those papers, All tab shows union of seed+user papers, so switching tabs after a search no longer shows empty results - Tests: add GET /papers/author, GET /papers/institution, and GET /papers?limit controller tests; add service-layer counterparts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… help modal table layout - Filter tabs (All/Seed/Added) now derive from state.nodes in real time instead of session-wide tracking lists — they always reflect what is actually in the graph, and disappear when the graph is cleared - Single-paper removal (badge click, detail panel Remove, context menu Remove) now uses removeNodeWithUndo: removes immediately, shows a 5.5-second toast with an Undo button that restores the paper and all its citation edges; batch Remove-all has no undo (too many links) - showToast refactored to accept an optional undoFn callback rendered as an inline button inside the toast - Help modal redesigned as a two-column table with section dividers, key badges left-aligned and descriptions right-aligned, replacing the old flex-row layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mixed-state bar button now reads 'Remove remaining' instead of 'Remove N', which is clearer about intent (remove those already in the graph, not a numeric count) - Batch removal (Remove remaining / Remove all from graph) now uses removeNodesAnimated: items fade and slide left over 0.55s then disappear; individual badge-click removal keeps the undo toast Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…node - removeNodeWithUndo now adds .removing to the sidebar item before calling removeNode so refreshBadges skips it; item fades-slides out over 0.55s then is removed from DOM; undo re-renders the results list with the paper restored - refreshBadges skips .removing items (selector :not(.removing)) - Context menu is now anchored to the node in simulation space: repositionContextMenu() converts ctxTarget.x/y through the D3 zoom transform to screen coords and is called on every ticked() tick, so the menu tracks the node as the force layout settles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add pointer-events: auto to #toast.visible so the Undo button is clickable when the toast is shown. Give the context menu danger item a light red background by default and a darker red on hover. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pers
UI:
- Right-click a sidebar item to open a context menu with Delete from
database; clicking it shows an inline Confirm / Cancel step before
the irreversible DELETE /papers/{id} call is made
- Deletes the node from the graph, removes it from localStorage, and
animates it out of the sidebar
- User-created papers are stored in localStorage (saveUserPaperToStorage)
so the Added tab persists across page reloads
- Filter-tab sidebar re-derives from graph state after batch removal
(filterTabActive flag + refreshFilterTabView helper) so stale Add-N
buttons no longer appear after removing all papers
- loadSample uses showAddAll:true so Remove all from graph is visible
immediately; removeNodesAnimated shows a 5.5 s undo toast
- Searching with an empty input clears results to an empty state
Test:
- PaperRepositoryTest: deleteById_detachesCitationRelationships verifies
that deleting a paper also detaches its CITES edges, leaving the cited
paper intact and no citing relationship remaining
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Citation flow (∿ toggle button, top-right): - Two particles per edge animate along each directed citation link, coloured by the source node's decade colour - Particles fade in/out at the node boundary and loop continuously - Uses requestAnimationFrame; stopped cleanly when toggled off BFS ripple (automatic on node click): - Computes BFS distances from the clicked node through the undirected citation graph (spreads both along and against citation direction) - Expanding rings are drawn at each reachable node, delayed by 130 ms per hop — amber for the source, blue for neighbours - Rings scale from r=18 to r=44 then fade out via D3 transition Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Repository: findPapersByTitle uses CONTAINS so "test" matches "dette er en test"; returns Flux<Paper> (all matches, not just first). Service/controller: updated signatures; endpoint always returns 200 with an array — empty array means no results, no more 404. Mock server: .find() → .filter() so all matching titles are returned. Frontend: removed allow404 for title; treats empty array as no results. Tests updated across all three layers; PaperRepositoryTest adds cases for partial match and case-insensitive matching. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GET /papers/fuzzy-title/{query} — word-level Levenshtein matching
with threshold ceil(wordLength / 3), so "graff" finds all graph papers
and "tset" finds "dette er en test".
Frontend: title search first tries CONTAINS; if 0 results it falls back
to the fuzzy endpoint and labels results "N approximate matches for '...'".
Mock server: matching fuzzy route using the same algorithm in JS.
Service: findPapersByTitleFuzzy filters paperRepository.findAll() using
the Levenshtein helper; skips query words shorter than 3 chars.
Tests: service (typo/no-match/contains-still-works) and controller
(fuzzy hit / fuzzy miss) added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR expands paper search capabilities (multi-result, case-insensitive title search + fuzzy title matching) and updates both backend and frontend behaviors around searching and interacting with results.
Changes:
- Backend: change title search to return multiple substring/case-insensitive matches; add a fuzzy title search endpoint with Levenshtein-based matching.
- Tests: update/add service/controller/repository tests for the new search behaviors and relationship deletion semantics.
- Frontend: enhance results interactions (add/remove all, undo removal, context menus), add help modal, and add citation-flow/selection animations.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/main/java/com/simonskodt/citenetwork/repositories/PaperRepository.java |
Updates title search query to return a Flux of case-insensitive substring matches. |
src/main/java/com/simonskodt/citenetwork/services/PaperService.java |
Adds fuzzy title search by scanning papers and applying a Levenshtein-based matcher. |
src/main/java/com/simonskodt/citenetwork/controllers/PaperController.java |
Adjusts /papers/title/{title} to return lists and adds /papers/fuzzy-title/{query}. |
src/test/java/com/simonskodt/citenetwork/services/PaperServiceTest.java |
Updates tests for multi-result title search and adds fuzzy-search tests. |
src/test/java/com/simonskodt/citenetwork/repositories/PaperRepositoryTest.java |
Adds title-search tests and a delete-relationship detachment test (currently with a compile-breaking method call). |
src/test/java/com/simonskodt/citenetwork/controllers/PaperControllerTest.java |
Updates controller tests to expect list responses; adds fuzzy endpoint and a questionable limit param test. |
src/main/resources/static/index.html |
Large UI updates: results bar actions, undo removal toast, help modal, context menus, and animations. |
mock-server.js |
Implements fuzzy-title route + Levenshtein matcher and changes title route to return arrays. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let allSeedPapers = []; // every paper loaded via the Load button this session | ||
| let currentVisible = []; // the currently rendered result items (for refreshBadges) | ||
| let filterTabActive = false; // true when sidebar shows graph contents, not search results |
There was a problem hiding this comment.
allSeedPapers is populated in loadSample(), but it isn’t read anywhere else in the file. If it’s not used for any UI/state logic yet, it’s dead state that can be removed; otherwise, consider integrating it into the filter/view logic so it has an effect.
There was a problem hiding this comment.
Fixed in commit 1db3986. refreshFilterTabView now has a dedicated "seed" branch (mirroring the existing "user" branch) that reads from allSeedPapers — showing all seed papers loaded this session, their total count, and how many are currently in the graph. The old dead allInGraph.filter(…) path for the seed case is removed.
| <!-- ── Help modal ─────────────────────────────────────────────────────────── --> | ||
| <div id="help-overlay"> | ||
| <div id="help-modal"> | ||
| <button id="help-close">✕</button> | ||
| <h2>How to use the Citation Network</h2> | ||
| <table class="help-table"> | ||
| <tbody> | ||
| <tr class="help-section-row"><td colspan="2">Navigating</td></tr> | ||
| <tr><td><span class="hk">Scroll</span></td><td>Zoom in / out</td></tr> | ||
| <tr><td><span class="hk">Drag background</span></td><td>Pan the graph</td></tr> | ||
| <tr><td><span class="hk">Click node</span></td><td>Select — see title, DOI, authors, and actions in the panel below</td></tr> | ||
| <tr><td><span class="hk">Right-click node</span></td><td>Context menu: expand citations or remove the node</td></tr> |
There was a problem hiding this comment.
The PR description focuses on backend paper search (fuzzy title + multi-result title search) and related tests, but this diff also introduces substantial new frontend behavior (help modal, context menus, removal/undo animations, citation flow animation, persistent edge drawing, localStorage persistence). Please update the PR description to reflect these UI changes as well, so reviewers and release notes capture the full scope.
There was a problem hiding this comment.
Updated in the PR description. The description now covers all frontend additions: help modal, context menus, add-all/remove-all with undo toast, citation-flow animation, selection animation, hold-to-draw edge creation, localStorage persistence, and seed filter integration.
| StepVerifier.create(paperRepository.findPaperByTitle("Citing")) | ||
| .verifyComplete(); | ||
|
|
||
| // Cited paper still exists — only the citing node was deleted | ||
| StepVerifier.create(paperRepository.findPaperByTitle("Cited")) | ||
| .assertNext(p -> Assertions.assertEquals("Cited", p.getTitle())) |
There was a problem hiding this comment.
PaperRepository no longer defines findPaperByTitle(...), but this new test (and the existing deleteById_removesPaper test above) still calls it. This will fail compilation and also no longer matches the updated title-search semantics (now returns a Flux). Update the assertions to use findPapersByTitle(...) (and adapt expectations accordingly), or reintroduce an exact-match repository method if that behavior is still needed for these delete tests.
There was a problem hiding this comment.
Fixed in commit b7a140e. Both deleteById_removesPaper and deleteById_detachesCitationRelationships now call findPapersByTitle (returning Flux<Paper>) and chain with .thenMany(...) / standalone StepVerifier.create(...) accordingly.
| public Flux<Paper> findPapersByTitleFuzzy(String query) { | ||
| String q = query.toLowerCase().trim(); | ||
| return paperRepository.findAll() | ||
| .filter(p -> isFuzzyTitleMatch(p.getTitle(), q)); | ||
| } |
There was a problem hiding this comment.
findPapersByTitleFuzzy currently does a full paperRepository.findAll() scan and runs Levenshtein on every paper title. This won’t scale as the dataset grows and can become an expensive endpoint to expose unauthenticated. Consider moving fuzzy matching into Neo4j (full-text index / APOC), or at least bounding the work (e.g., limit candidate set, cap max results, short-circuit once enough matches are found).
| function fuzzyTitleMatch(title, query) { | ||
| const t = title.toLowerCase(), q = query.toLowerCase(); | ||
| if (t.includes(q)) return true; | ||
| const qWords = q.split(/\s+/).filter(w => w.length >= 3); | ||
| const tWords = t.split(/\s+/); | ||
| return qWords.some(qw => | ||
| tWords.some(tw => { | ||
| const thresh = Math.ceil(qw.length / 3); | ||
| return levenshtein(qw, tw) <= thresh; |
There was a problem hiding this comment.
fuzzyTitleMatch will return true for an empty/whitespace query because t.includes("") is always true. That means /papers/fuzzy-title/%20 would match every paper in the mock server. Add a guard for blank queries (e.g., return false early or treat it as no results) to match the intended API behavior and avoid accidental full-dataset responses.
There was a problem hiding this comment.
Fixed in commit b7a140e. fuzzyTitleMatch now trims and lowercases the query first, then returns false early when the result is empty — blank or whitespace-only queries no longer match every paper.
| @Test | ||
| void GET_papers_withLimitParam_returnsResults() { | ||
| when(paperService.findFirstTenPapers()).thenReturn( | ||
| Flux.just(paper(1L, "A"), paper(2L, "B"), paper(3L, "C"))); | ||
|
|
||
| webTestClient.get().uri("/papers?limit=3") | ||
| .accept(MediaType.APPLICATION_JSON) | ||
| .exchange() | ||
| .expectStatus().isOk() | ||
| .expectBodyList(Paper.class).hasSize(3); | ||
| } |
There was a problem hiding this comment.
This test name implies /papers?limit=3 changes controller behavior, but PaperController.findFirstTenPapers() does not read a limit query param (and the mock only stubs findFirstTenPapers()), so the test would still pass even if limit is ignored. Either implement @RequestParam int limit support in the controller/service/repo, or rename/adjust the test to reflect the actual behavior and verify the expected service interaction.
There was a problem hiding this comment.
Fixed in commit b7a140e. Renamed the test to GET_papers_returnsFirstTenPapers and changed the URI to /papers (no ?limit=3). The test now accurately documents that the controller delegates to findFirstTenPapers() with no limit parameter.
….java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/simonskodt/cite-network/sessions/64b36dad-3b09-4b5f-8301-5ee6371219da Co-authored-by: simonskodt <74598690+simonskodt@users.noreply.github.com>
Agent-Logs-Url: https://github.com/simonskodt/cite-network/sessions/64b36dad-3b09-4b5f-8301-5ee6371219da Co-authored-by: simonskodt <74598690+simonskodt@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| StepVerifier.create(paperRepository.deleteById(1L) | ||
| .then(paperRepository.findPaperByTitle("Deletable"))) | ||
| .verifyComplete(); |
There was a problem hiding this comment.
This test still calls paperRepository.findPaperByTitle(...), but the repository interface was changed to findPapersByTitle(...) returning a Flux. As written this will not compile and also no longer matches the updated title-search semantics. Update the test to use findPapersByTitle(...) and adapt the expectations (e.g., expect an empty Flux after deletion).
| StepVerifier.create(paperRepository.findPaperByTitle("Citing")) | ||
| .verifyComplete(); | ||
|
|
||
| // Cited paper still exists — only the citing node was deleted | ||
| StepVerifier.create(paperRepository.findPaperByTitle("Cited")) | ||
| .assertNext(p -> Assertions.assertEquals("Cited", p.getTitle())) |
There was a problem hiding this comment.
These assertions use paperRepository.findPaperByTitle(...), but that method no longer exists after the title search was changed to return a Flux from findPapersByTitle(...). This will fail compilation; switch to findPapersByTitle(...) and adjust the checks (e.g., verify no results for "Citing" and at least one result for "Cited").
| return paperRepository.findAll() | ||
| .filter(p -> isFuzzyTitleMatch(p.getTitle(), q)); | ||
| } |
There was a problem hiding this comment.
findPapersByTitleFuzzy() currently scans the entire dataset via paperRepository.findAll() and then runs an in-memory Levenshtein match. That makes the endpoint O(N * wordPairs * m*n) and will not scale beyond small graphs. Consider adding a server-side prefilter/limit (e.g., first query candidates with CONTAINS on some token(s), cap results with .take(n), or implement a Neo4j-side approach) to avoid full-database scans per request.
| @GetMapping("/title/{title}") | ||
| public Mono<Paper> findPaperByTitle(@PathVariable String title) { | ||
| return paperService.findPaperByTitle(title) | ||
| .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Paper not found"))); | ||
| public Flux<Paper> findPapersByTitle(@PathVariable String title) { | ||
| return paperService.findPapersByTitle(title); |
There was a problem hiding this comment.
Changing /papers/title/{title} from returning a single paper (404 when not found) to returning a Flux (200 + empty list) is a breaking API behavior change for existing clients. If backward compatibility matters, consider keeping the old exact-match endpoint (or adding a ?strict=true//title/exact/... variant) while introducing the multi-result search separately, and document the new semantics.
| <!-- ── Help modal ─────────────────────────────────────────────────────────── --> | ||
| <div id="help-overlay"> | ||
| <div id="help-modal"> | ||
| <button id="help-close">✕</button> | ||
| <h2>How to use the Citation Network</h2> | ||
| <table class="help-table"> | ||
| <tbody> | ||
| <tr class="help-section-row"><td colspan="2">Navigating</td></tr> | ||
| <tr><td><span class="hk">Scroll</span></td><td>Zoom in / out</td></tr> | ||
| <tr><td><span class="hk">Drag background</span></td><td>Pan the graph</td></tr> | ||
| <tr><td><span class="hk">Click node</span></td><td>Select — see title, DOI, authors, and actions in the panel below</td></tr> | ||
| <tr><td><span class="hk">Right-click node</span></td><td>Context menu: expand citations or remove the node</td></tr> | ||
| <tr class="help-section-row"><td colspan="2">Adding papers</td></tr> | ||
| <tr><td><span class="hk">Load</span></td><td>Load N seed papers into the graph; set the count with the number input</td></tr> | ||
| <tr><td><span class="hk">Search bar</span></td><td>Find by title, author, year, or institution — results appear in the sidebar; click to add</td></tr> | ||
| <tr><td><span class="hk">+ New</span></td><td>Create a new paper manually, or paste text for AI extraction</td></tr> | ||
| <tr><td><span class="hk">Bulk</span></td><td>Import multiple papers by DOI via CrossRef, or free-text via AI</td></tr> | ||
| <tr class="help-section-row"><td colspan="2">Drawing citation arrows</td></tr> | ||
| <tr><td><span class="hk">Hold ~400 ms</span></td><td>Enter drag mode — drag the dashed arrow to a target node</td></tr> | ||
| <tr><td><span class="hk">Double-click node</span></td><td>Enter persistent draw mode — then click any other node to connect</td></tr> | ||
| <tr><td><span class="hk">Esc</span></td><td>Cancel drawing at any time</td></tr> | ||
| <tr class="help-section-row"><td colspan="2">Exploring connections</td></tr> | ||
| <tr><td><span class="hk">Cited by this</span></td><td>Expand outward — load papers this node cites</td></tr> | ||
| <tr><td><span class="hk">Citing this</span></td><td>Expand inward — load papers that cite this node</td></tr> | ||
| <tr><td><span class="hk">All / Seed / Added</span></td><td>Filter tabs — show papers in the graph filtered by source</td></tr> | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
The PR description is focused on backend search changes/tests, but this PR also includes substantial frontend/UI updates in index.html (help modal, node/sidebar context menus, undo removal animations, citation flow particles, persistent edge drawing, localStorage persistence, etc.). Please update the PR description to reflect the full scope so reviewers/release notes aren’t misled.
- PaperRepositoryTest: use findPapersByTitle (Flux) instead of removed findPaperByTitle in deleteById tests - PaperControllerTest: rename test to reflect controller always returns first 10 papers regardless of any limit param - mock-server.js: guard fuzzyTitleMatch against blank/whitespace queries returning false early Agent-Logs-Url: https://github.com/simonskodt/cite-network/sessions/b1a4650b-c337-4cbc-8535-49c5a9b33879 Co-authored-by: simonskodt <74598690+simonskodt@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This pull request enhances the paper search functionality by introducing fuzzy title matching and improving the title-based search to support multiple results and case-insensitive queries. It also adds comprehensive tests for these new and updated features, ensuring robust search capabilities and correctness. The most significant changes are grouped below.
Search Functionality Improvements:
/papers/fuzzy-title/{query}) to the backend, allowing users to find papers even with typos or approximate matches in the title. This includes a Levenshtein distance-based algorithm for fuzzy matching in both the backend (PaperService) and mock server (mock-server.js). [1] [2] [3] [4]Testing Enhancements:
These changes make the paper search APIs more flexible and user-friendly, and ensure correctness through thorough testing.