Skip to content

Teditor#18

Merged
thejonathangu merged 4 commits into
mainfrom
teditor
Feb 7, 2026
Merged

Teditor#18
thejonathangu merged 4 commits into
mainfrom
teditor

Conversation

@tedgershon

@tedgershon tedgershon commented Feb 7, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Book discovery search functionality to find and select literary works
    • Book PDF upload with automatic location extraction capability
    • Era-based filtering on literary map to show relevant locations
    • Route visualization connecting literary points by historical period
  • UI/UX Improvements

    • Redesigned sidebar layout and content organization
    • Enhanced map styling with interactive controls
    • Updated map library to latest version

@tedgershon tedgershon requested a review from Copilot February 7, 2026 05:20
@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a new Librarian agent for searching books via Open Library, adds a BookSearch React component for book discovery, refactors the frontend App and MapComponent with updated styling and props, removes mood metadata from literary data, and simplifies the conductor orchestration by eliminating chain-of-thought instrumentation.

Changes

Cohort / File(s) Summary
Frontend HTML & Build
frontend/index.html, frontend/main.jsx, frontend/vite.config.js
Updated Mapbox GL JS CSS from v3.3.0 to v3.18.1, adjusted body/root sizing and styling, added CSS import to main.jsx, and configured new /orchestrate proxy route in Vite dev server.
Frontend React Components
frontend/src/App.jsx, frontend/src/components/BookSearch.jsx, frontend/src/components/MapComponent.jsx
Replaced App component state and layout to integrate new BookSearch component; added selectedBook and uploadedBookLocations state; updated MapComponent props (uploadedGeoJSON → uploadedBookLocations) and added era-based filtering and style overrides; BookSearch is a new debounced search component with book selection callback.
Frontend Data & API
frontend/src/data/literaryPoints.js, frontend/src/api/archivistClient.js
Removed mood property from all literaryGeoJSON features; consolidated archivistClient API by removing fetchVibeSearch and introducing fetchLibrarianSearch and new uploadBookPDF function.
Backend Librarian Agent
mcp-servers/librarian/apps.py, mcp-servers/librarian/urls.py, mcp-servers/librarian/views.py
New Django app module implementing book discovery: includes LibrarianConfig, URL routing for /search endpoint, and views with _librarian_search() function querying Open Library API and returning structured book results.
Backend Core Configuration & Orchestration
mcp-servers/core/settings.py, mcp-servers/core/urls.py, mcp-servers/core/views.py, mcp-servers/core/conductor.py
Registered librarian app in INSTALLED_APPS, added librarian route to urlpatterns, added LibrarianAgent entry to MCP agents list, and refactored conductor to support "search" action delegating to Librarian with removal of chain-of-thought instrumentation and per-agent CoT logging.
Backend Knowledge Base
mcp-servers/archivist/knowledge_base.py
Removed mood attribute from eight knowledge base entries (jlc-san-francisco, jlc-chinatown, hr-harlem, hr-apollo, hr-cathedral, cr-montgomery, cr-birmingham, cr-lincoln-memorial).

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Frontend as BookSearch Component
    participant Backend as Librarian API
    participant OpenLib as Open Library API

    User->>Frontend: Enter search query
    Frontend->>Frontend: Debounce (400ms)
    Frontend->>Backend: POST /tools/librarian/search<br/>(query, limit)
    Backend->>OpenLib: GET search.json?title=query
    OpenLib-->>Backend: Book results with metadata
    Backend->>Backend: Extract & normalize fields<br/>(title, authors, cover_url, etc.)
    Backend-->>Frontend: JSON response with book list
    Frontend->>Frontend: Display results<br/>(images, titles, authors, years)
    User->>Frontend: Select a book
    Frontend->>Frontend: Highlight selection
    Frontend->>User: Trigger onBookSelect callback
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A Librarian joins the quest, searching for tales untold,
BookSearch now guides us through libraries of gold,
No moods remain, just facts and locations bright,
The orchestration flows simpler, streamlined with delight!
hops excitedly 📚✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Teditor' appears to be a name or abbreviation but does not clearly describe the actual changes made in the pull request, which include significant refactoring (adding book search, librarian agent, MapComponent redesign, Mapbox version bump, etc.). Use a descriptive title that summarizes the main changes, such as 'Add book search and librarian agent functionality' or 'Refactor UI with book discovery and librarian integration'.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch teditor

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new LibrarianAgent (Open Library search tool) and integrates it into the existing MCP orchestration + frontend UI, alongside a fairly large refactor of the map UI and Conductor responses.

Changes:

  • Added a Django librarian app exposing /tools/librarian/search (and wired it into core settings/urls and the index agent list).
  • Updated the Conductor to support a "search" action shortcut and simplified orchestration responses (removed chain-of-thought).
  • Frontend: added a new BookSearch widget + significant MapComponent refactor (routes/labels/filtering), and adjusted Mapbox CSS loading/proxy config.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
mcp-servers/librarian/views.py Implements Open Library search tool + HTTP endpoint.
mcp-servers/librarian/urls.py Wires search endpoint for the new agent.
mcp-servers/librarian/apps.py Registers the Django app config.
mcp-servers/librarian/init.py Initializes the librarian module/package.
mcp-servers/core/views.py Adds LibrarianAgent metadata to the index response.
mcp-servers/core/urls.py Includes librarian.urls under /tools/librarian/.
mcp-servers/core/settings.py Adds librarian to INSTALLED_APPS.
mcp-servers/core/conductor.py Adds "action":"search" path + removes chain-of-thought + tweaks timeline handling.
mcp-servers/archivist/knowledge_base.py Removes mood fields from knowledge base entries.
frontend/vite.config.js Proxies /orchestrate to the Django backend in dev.
frontend/src/main.jsx Imports Mapbox GL CSS via bundler.
frontend/src/data/literaryPoints.js Removes mood fields from map data points.
frontend/src/components/MapComponent.jsx Major map refactor (route lines, labels, filters, stylist overrides, upload layer changes).
frontend/src/components/BookSearch.jsx New UI component to query LibrarianAgent + display selectable results.
frontend/src/api/archivistClient.js Adds fetchLibrarianSearch and refactors upload + removes VibeSearch helper.
frontend/src/App.jsx Integrates BookSearch + updated upload wiring + removes vibe search/detective UI, passes new map props.
frontend/index.html Updates Mapbox CSS link version and page sizing styles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/main.jsx
@@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "mapbox-gl/dist/mapbox-gl.css";

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapbox CSS is now imported here, but frontend/index.html also includes Mapbox CSS from a CDN. This can lead to duplicated CSS and version mismatches. Prefer a single approach (bundled import or CDN), and keep CSS/JS versions aligned.

Suggested change
import "mapbox-gl/dist/mapbox-gl.css";

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Comment thread frontend/src/App.jsx
📄 Upload Book PDF — AI Location Extraction
</p>
<BookUpload
accentColor={eraColor}

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BookUpload doesn’t accept or use an accentColor prop, but it’s passed here. Either remove this prop, or update BookUpload to support theme coloring to avoid dead/incorrect props.

Suggested change
accentColor={eraColor}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Comment thread frontend/src/App.jsx
/* ── Book selection from LibrarianAgent search ──────────────── */
const handleBookSelect = useCallback((book) => {
setSelectedBook(book);
console.log("Selected book for processing:", book);

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover debug logging (console.log) will spam the console in normal usage. Consider removing it or gating it behind a dev flag.

Suggested change
console.log("Selected book for processing:", book);
if (process.env.NODE_ENV !== "production") {
console.log("Selected book for processing:", book);
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Comment on lines +64 to +73
useEffect(() => {
if (mapRef.current) return;

const map = new mapboxgl.Map({
container: mapContainer.current,
style: MAP_STYLES.Satellite,
center: [-95.7, 37.0],
zoom: 3.5,
pitch: 15,
});

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map initialization runs even when MAPBOX_TOKEN is empty, but in that case the component returns a fallback UI that never renders the map container. This means mapContainer.current will be null and new mapboxgl.Map({ container: null }) can throw at runtime. Guard the init effect (and/or the component) so the map is only created when a token exists and the container ref is available.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

import json
import time
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as_completed is imported but not used, which will trigger lint/static-analysis failures. Either remove the import or iterate futures via as_completed (which also avoids waiting in submission order).

Suggested change
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import ThreadPoolExecutor

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Comment on lines +78 to +111
if action == "search":
query = body.get("query")
if not query:
return JsonResponse({"error": "query is required for search action"}, status=400)
limit = body.get("limit", 10)
t0 = time.perf_counter()
try:
result = _librarian_search(query, limit=limit)
elapsed = round((time.perf_counter() - t0) * 1000)
total = round((time.perf_counter() - t_start) * 1000)
return JsonResponse({
"librarian": result,
"timeline": [{
"agent": "LibrarianAgent",
"tool": "search_books",
"status": "success",
"elapsed_ms": elapsed,
}],
"total_ms": total,
})
except Exception as exc:
elapsed = round((time.perf_counter() - t0) * 1000)
total = round((time.perf_counter() - t_start) * 1000)
return JsonResponse({
"error": str(exc),
"timeline": [{
"agent": "LibrarianAgent",
"tool": "search_books",
"status": "error",
"elapsed_ms": elapsed,
"error": str(exc),
}],
"total_ms": total,
}, status=502)

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the action == "search" path, all exceptions are turned into a 502. This misclassifies client errors (e.g. whitespace-only query causes _librarian_search to raise ValueError) and also exposes raw exception strings. Mirror librarian.views.search handling here: return 400 for ValueError, 502 for httpx request/status errors, and use a generic message for unexpected exceptions.

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +186
m.on("click", "literary-markers", (e) => {
const feature = e.features[0];
const coords = feature.geometry.coordinates.slice();
m.flyTo({ center: coords, zoom: 14, speed: 1.2, pitch: 45 });
if (onMarkerClickRef.current) onMarkerClickRef.current(feature);
});
m.on("mouseenter", "literary-markers", () => {
m.getCanvas().style.cursor = "pointer";
});
m.on("mouseleave", "literary-markers", () => {
m.getCanvas().style.cursor = "";
});

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addAllLayers registers Mapbox event handlers (map.on(...)) each time it runs. Because it is invoked on every style.load, switching styles can accumulate duplicate click/hover handlers on the same layer ids. Consider registering handlers once (outside addAllLayers) or calling map.off before re-registering.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Comment on lines +333 to +346
// Click handlers
map.on("click", "uploaded-markers", (e) => {
const feature = e.features[0];
const coords = feature.geometry.coordinates.slice();
map.flyTo({ center: coords, zoom: 14, speed: 1.2, pitch: 45 });
if (onMarkerClickRef.current) onMarkerClickRef.current(feature);
});

map.on("mouseenter", "uploaded-markers", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "uploaded-markers", () => {
map.getCanvas().style.cursor = "";
});

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effect adds new click/mouseenter/mouseleave handlers for uploaded-markers each time uploadedBookLocations changes, but it never removes prior handlers. If the user uploads multiple times, clicks can fire multiple times. Use map.off with stable handler functions (or register once and update via refs) when re-adding the layer.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

@@ -137,48 +141,24 @@ def cot(agent, step, detail=""):

for key, future in futures.items():

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current loop waits on future.result() in dict insertion order, which can block on a slow agent even if others have already completed. Since as_completed is already imported, consider iterating with it and mapping futures back to keys so timeline entries are recorded as tasks finish (and you don’t unnecessarily delay processing).

Suggested change
for key, future in futures.items():
# Process futures as they complete to avoid blocking on slower agents
future_to_key = {future: key for key, future in futures.items()}
for future in as_completed(futures.values()):
key = future_to_key[future]

Copilot uses AI. Check for mistakes.
Comment thread frontend/index.html
Comment on lines 7 to 10
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
href="https://api.mapbox.com/mapbox-gl-js/v3.18.1/mapbox-gl.css"
rel="stylesheet"
/>

Copilot AI Feb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app now imports Mapbox CSS via mapbox-gl/dist/mapbox-gl.css (bundled), but this file still links to a CDN Mapbox CSS version (and the version differs from mapbox-gl in package.json). To avoid duplicated/mismatched styles, prefer one source: either remove this <link> and rely on the bundled CSS, or align/pin versions so CSS and JS match.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mcp-servers/core/views.py (1)

8-22: ⚠️ Potential issue | 🟡 Minor

Update discovery metadata to match the new agent count.
The payload still claims “3 Specialist MCP Agents,” but LibrarianAgent was added. This can confuse clients that surface the discovery metadata.

🛠️ Suggested fix
-        "architecture": "Conductor → 3 Specialist MCP Agents",
+        "architecture": "Conductor → 4 Specialist MCP Agents (incl. Librarian)",
🤖 Fix all issues with AI agents
In `@frontend/index.html`:
- Around line 12-14: The CSS sets `#root` { width: 100vw; height: 100vh; } which
can cause horizontal scroll due to scrollbar width; change `#root` to use width:
100% and keep height behavior via 100vh or better by setting html and body to
height: 100% and then `#root` to height: 100%; update the stylesheet by replacing
100vw with 100% on the `#root` selector and add/ensure html and body selectors
include height: 100% (and optionally overflow-x: hidden on body if you want to
forcibly prevent horizontal scroll).
- Around line 7-9: The Mapbox GL CSS in index.html (the link element referencing
mapbox-gl.css) conflicts with the npm package version (^3.3.0 in package.json)
and is also duplicated by an import in frontend/src/main.jsx; fix by choosing
one approach: either update the HTML link href to match the installed package
version (v3.3.0) or upgrade the npm dependency to v3.18.1 so both match, and
remove the duplicate CSS load (either delete the CDN <link> in index.html or
remove the CSS import in frontend/src/main.jsx) so the stylesheet is only loaded
once.

In `@frontend/src/components/BookSearch.jsx`:
- Around line 1-47: Add a stale-response guard and clean up debounce timers on
unmount: generate a request counter ref (e.g., latestReqRef) and increment it
inside handleInputChange/doSearch; capture the current id at the start of
doSearch and only call setResults/setError/setLoading if the id matches
latestReqRef.current (this prevents out-of-order responses from updating state).
Also add a useEffect cleanup that clears debounceRef.current timeout and, if you
use AbortController with fetchLibrarianSearch, abort the controller on unmount
(or otherwise mark latestReqRef to a new value) so no pending callbacks run
after unmount; update doSearch to accept/use an optional AbortSignal or check
the request id before setting state. Ensure references to debounceRef, doSearch,
handleInputChange, and latestReqRef are used consistently.

In `@frontend/src/components/MapComponent.jsx`:
- Around line 370-423: The popup HTML is built by interpolating unescaped
user/API data (popupContent -> properties.*, deepContext.historical_context)
then passed to mapboxgl.Popup.setHTML, which allows XSS; fix by sanitizing all
interpolated values (properties.era, properties.title, properties.book,
properties.year, properties.quote, deepContext.historical_context, and any
coords-derived strings) before interpolation — either implement a small
escapeHtml utility and call it for each inserted value, or instead construct the
popup with DOM nodes and use setDOMContent to assign text via textContent (avoid
setHTML with raw strings); update the popup creation (where popupRef.current is
assigned and setHTML is called) to use the chosen safe approach and ensure
accentColor remains derived from a whitelist of expected era values rather than
raw input.
- Around line 286-367: The uploaded-markers event handlers are re-attached on
every uploadedBookLocations update causing duplicate listeners; modify the
useEffect that defines addUploadedLayer to register named handler functions (or
store references) for the "click", "mouseenter", and "mouseleave" events on
"uploaded-markers" and remove them in the useEffect cleanup (use map.off for
"uploaded-markers" with the same handler references) and also remove the
"uploaded-markers" and "uploaded-labels" layers and "uploaded-points" source in
that cleanup; ensure addUploadedLayer still adds layers/sources only once (use
map.isStyleLoaded()/map.once as currently) and that the cleanup also unregisters
the style.load listener if you attach one.

In `@mcp-servers/core/conductor.py`:
- Around line 75-111: Ensure conductor.py validates and normalizes the search
inputs before calling _librarian_search: strip and reject empty query (use the
existing action/query variables) and validate/convert limit to an int with
sensible bounds (e.g., default 10, clamp to 1..100 or similar); return
JsonResponse with status 400 for input validation failures. Narrow the exception
handling around the _librarian_search call: catch ValueError specifically and
return a 400 error with the validation message, and catch other exceptions
(generic) as before returning 502; preserve timing calculations (t0, t_start)
and include the same timeline structure but use the appropriate status/error
codes per error type.

In `@mcp-servers/librarian/views.py`:
- Around line 81-88: The JSON-parsing block uses json.loads(request.body) and
then calls body.get("query") which will raise AttributeError if the parsed body
is not an object; add a type guard after parsing to verify isinstance(body,
dict) (or that body has .get) and return JsonResponse({"error": "JSON object
required"}, status=400) if it is not, so subsequent uses of body.get(...) are
safe; update the code around the json.loads(...) / body variable and the
JsonResponse error paths accordingly.
- Around line 90-94: The request handler currently reads limit =
body.get("limit", 10) and passes it directly to _librarian_search; validate and
bound it before that call: ensure the incoming limit is an integer (coerce
numeric strings or reject non-numeric), enforce a sane range (e.g., min 1, max
100) and clamp or return a 400 Bad Request on invalid input, and then pass the
validated/clamped limit to _librarian_search; update the handler's validation
logic around the limit variable and add explicit error handling for bad
types/ranges.
🧹 Nitpick comments (2)
frontend/src/api/archivistClient.js (1)

5-17: Normalize query and limit before calling the Librarian endpoint.
This prevents accidental empty/whitespace searches and keeps limit in a safe range.

🛠️ Suggested fix
 export async function fetchLibrarianSearch(query, limit = 10) {
+  const safeQuery = String(query ?? "").trim();
+  if (!safeQuery) throw new Error("Query is required");
+  const safeLimit = Number.isFinite(limit)
+    ? Math.max(1, Math.min(limit, 20))
+    : 10;
   const res = await fetch(`${MCP_BASE_URL}/tools/librarian/search`, {
     method: "POST",
     headers: { "Content-Type": "application/json" },
-    body: JSON.stringify({ query, limit }),
+    body: JSON.stringify({ query: safeQuery, limit: safeLimit }),
   });
frontend/src/components/MapComponent.jsx (1)

231-240: Avoid returning a value from the forEach callback.
This matches the linter expectation and avoids accidental return values.

🛠️ Suggested fix
-      eraFeatures.forEach((f) => bounds.extend(f.geometry.coordinates));
+      eraFeatures.forEach((f) => {
+        bounds.extend(f.geometry.coordinates);
+      });

Comment thread frontend/index.html
Comment on lines 7 to 9
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
href="https://api.mapbox.com/mapbox-gl-js/v3.18.1/mapbox-gl.css"
rel="stylesheet"

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Mapbox GL JS version in package.json
echo "=== Checking package.json for Mapbox GL version ==="
find . -name "package.json" -type f | head -5 | xargs grep -l "mapbox-gl" 2>/dev/null | head -1 | xargs cat | grep -A 2 -B 2 "mapbox-gl"

echo ""
echo "=== Searching for mapbox-gl imports in codebase ==="
rg "mapbox-gl" -t js -t jsx -t ts -t tsx --max-count 10

echo ""
echo "=== Checking for any Mapbox imports in frontend directory ==="
rg "mapbox" frontend/ --max-count 10 2>/dev/null || echo "No mapbox imports found in frontend/"

Repository: thejonathangu/tartanhacks

Length of output: 2563


Fix Mapbox GL CSS/JS version mismatch.
The CSS version in the HTML (v3.18.1 from CDN) does not match the npm package version (^3.3.0 in package.json). The JS code imports from the npm package while the HTML links to the CDN, causing potential rendering conflicts.

Update the HTML link to use v3.3.0 to match the installed package version, or upgrade the npm package to v3.18.1. Additionally, the CSS is being loaded twice (CDN link in index.html and import in frontend/src/main.jsx)—remove the redundant source.

🤖 Prompt for AI Agents
In `@frontend/index.html` around lines 7 - 9, The Mapbox GL CSS in index.html (the
link element referencing mapbox-gl.css) conflicts with the npm package version
(^3.3.0 in package.json) and is also duplicated by an import in
frontend/src/main.jsx; fix by choosing one approach: either update the HTML link
href to match the installed package version (v3.3.0) or upgrade the npm
dependency to v3.18.1 so both match, and remove the duplicate CSS load (either
delete the CDN <link> in index.html or remove the CSS import in
frontend/src/main.jsx) so the stylesheet is only loaded once.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread frontend/index.html
Comment on lines 12 to +14
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: system-ui, sans-serif; }
#root { width: 100%; height: 100%; }
body { font-family: system-ui, sans-serif; }
#root { width: 100vw; height: 100vh; }

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid 100vw width to prevent horizontal scrollbars.
100vw includes scrollbar width and can introduce horizontal overflow; prefer 100% with explicit html/body height.

🧩 Proposed CSS adjustment
-      body { font-family: system-ui, sans-serif; }
-      `#root` { width: 100vw; height: 100vh; }
+      html, body { height: 100%; }
+      body { font-family: system-ui, sans-serif; }
+      `#root` { width: 100%; height: 100%; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: system-ui, sans-serif; }
#root { width: 100%; height: 100%; }
body { font-family: system-ui, sans-serif; }
#root { width: 100vw; height: 100vh; }
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body { font-family: system-ui, sans-serif; }
`#root` { width: 100%; height: 100%; }
🤖 Prompt for AI Agents
In `@frontend/index.html` around lines 12 - 14, The CSS sets `#root` { width: 100vw;
height: 100vh; } which can cause horizontal scroll due to scrollbar width;
change `#root` to use width: 100% and keep height behavior via 100vh or better by
setting html and body to height: 100% and then `#root` to height: 100%; update the
stylesheet by replacing 100vw with 100% on the `#root` selector and add/ensure
html and body selectors include height: 100% (and optionally overflow-x: hidden
on body if you want to forcibly prevent horizontal scroll).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +1 to +47
import React, { useState, useCallback, useRef } from "react";
import { fetchLibrarianSearch } from "../api/archivistClient";

/**
* BookSearch — A search widget that queries the LibrarianAgent
* (Open Library) and renders selectable book results.
*
* Props:
* - onBookSelect(book) — called when the user clicks a book result
* - accentColor — theme accent (defaults to #4ecdc4)
*/
export default function BookSearch({ onBookSelect, accentColor = "#4ecdc4" }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [selectedKey, setSelectedKey] = useState(null);
const debounceRef = useRef(null);

const doSearch = useCallback(async (q) => {
if (!q.trim()) {
setResults(null);
setError(null);
return;
}
setLoading(true);
setError(null);
setSelectedKey(null);
try {
const data = await fetchLibrarianSearch(q, 8);
setResults(data);
} catch (err) {
console.error("LibrarianAgent search failed:", err);
setError("Search failed — please try again.");
setResults(null);
} finally {
setLoading(false);
}
}, []);

const handleInputChange = (e) => {
const val = e.target.value;
setQuery(val);
// Debounce 400ms
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => doSearch(val), 400);
};

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f -name "BookSearch.jsx" --type f

Repository: thejonathangu/tartanhacks

Length of output: 299


🏁 Script executed:

find . -name "BookSearch.jsx" -type f

Repository: thejonathangu/tartanhacks

Length of output: 109


🏁 Script executed:

cat -n ./frontend/src/components/BookSearch.jsx

Repository: thejonathangu/tartanhacks

Length of output: 11221


Prevent stale search results and clear debounce on unmount.
Fast typing can return out-of-order results because concurrent requests may complete in any order without a stale-response guard. Pending debounce timers can also fire after unmount, triggering state updates on an unmounted component.

🛠️ Suggested fix
-import React, { useState, useCallback, useRef } from "react";
+import React, { useState, useCallback, useRef, useEffect } from "react";
@@
   const [selectedKey, setSelectedKey] = useState(null);
   const debounceRef = useRef(null);
+  const requestSeq = useRef(0);
@@
   const doSearch = useCallback(async (q) => {
+    const seq = ++requestSeq.current;
     if (!q.trim()) {
+      requestSeq.current += 1;
       setResults(null);
       setError(null);
       return;
     }
@@
     try {
       const data = await fetchLibrarianSearch(q, 8);
+      if (seq !== requestSeq.current) return;
       setResults(data);
     } catch (err) {
@@
   }, []);
+
+  useEffect(() => {
+    return () => {
+      if (debounceRef.current) clearTimeout(debounceRef.current);
+      requestSeq.current += 1;
+    };
+  }, []);
🤖 Prompt for AI Agents
In `@frontend/src/components/BookSearch.jsx` around lines 1 - 47, Add a
stale-response guard and clean up debounce timers on unmount: generate a request
counter ref (e.g., latestReqRef) and increment it inside
handleInputChange/doSearch; capture the current id at the start of doSearch and
only call setResults/setError/setLoading if the id matches latestReqRef.current
(this prevents out-of-order responses from updating state). Also add a useEffect
cleanup that clears debounceRef.current timeout and, if you use AbortController
with fetchLibrarianSearch, abort the controller on unmount (or otherwise mark
latestReqRef to a new value) so no pending callbacks run after unmount; update
doSearch to accept/use an optional AbortSignal or check the request id before
setting state. Ensure references to debounceRef, doSearch, handleInputChange,
and latestReqRef are used consistently.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +286 to +367
// ── Uploaded book locations from PDF ──
useEffect(() => {
const map = mapRef.current;
if (!map || !uploadedBookLocations) return;

const addUploadedLayer = () => {
// Remove old layers if they exist
if (map.getLayer("uploaded-markers")) map.removeLayer("uploaded-markers");
if (map.getLayer("uploaded-labels")) map.removeLayer("uploaded-labels");
if (map.getSource("uploaded-points")) map.removeSource("uploaded-points");

map.addSource("uploaded-points", {
type: "geojson",
data: uploadedBookLocations,
});

map.addLayer({
id: "uploaded-markers",
type: "circle",
source: "uploaded-points",
paint: {
"circle-radius": 10,
"circle-color": "#b388ff",
"circle-stroke-width": 2,
"circle-stroke-color": "#ffffff",
"circle-opacity": 0.9,
},
});

map.addLayer({
id: "uploaded-labels",
type: "symbol",
source: "uploaded-points",
layout: {
"text-field": ["get", "title"],
"text-size": 11,
"text-offset": [0, 1.8],
"text-anchor": "top",
"text-max-width": 12,
},
paint: {
"text-color": "#b388ff",
"text-halo-color": "#000",
"text-halo-width": 1,
},
});

// Click handlers
map.on("click", "uploaded-markers", (e) => {
const feature = e.features[0];
const coords = feature.geometry.coordinates.slice();
map.flyTo({ center: coords, zoom: 14, speed: 1.2, pitch: 45 });
if (onMarkerClickRef.current) onMarkerClickRef.current(feature);
});

map.on("mouseenter", "uploaded-markers", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "uploaded-markers", () => {
map.getCanvas().style.cursor = "";
});

// Fit map to show all uploaded points
if (uploadedBookLocations.features.length > 0) {
const bounds = uploadedBookLocations.features.reduce(
(b, f) => {
return b.extend(f.geometry.coordinates);
},
new mapboxgl.LngLatBounds(
uploadedBookLocations.features[0].geometry.coordinates,
uploadedBookLocations.features[0].geometry.coordinates,
),
);
map.fitBounds(bounds, { padding: 80, maxZoom: 12 });
}
};

if (map.isStyleLoaded()) {
addUploadedLayer();
} else {
map.once("style.load", addUploadedLayer);
}

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd frontend/src/components && wc -l MapComponent.jsx

Repository: thejonathangu/tartanhacks

Length of output: 89


🏁 Script executed:

cd frontend/src/components && sed -n '280,380p' MapComponent.jsx

Repository: thejonathangu/tartanhacks

Length of output: 3224


🌐 Web query:

Mapbox GL JS map.on() event handler duplication stacking behavior

💡 Result:

map.on(type, handler) stacks: every time you call it, Mapbox GL JS registers another listener, so the handler will run multiple times per event if you accidentally attach it repeatedly (for example, inside a React render/effect that re-runs). Mapbox GL JS’ event system is based on Evented (“bind and unbind listeners”) and does not imply automatic de-duplication. [1]

How to avoid / undo duplicates:

  • Detach explicitly with the same function reference (anonymous inline functions can’t be removed because you can’t pass the same reference back):

    function onClick(e) { /* ... */ }
    map.on('click', onClick);
    
    // later
    map.off('click', onClick);

    (This is the common fix when you see “double click handler” type bugs.) [2]

  • Use map.once(...) if you only want it to run one time. [2]

  • In component frameworks, attach in an effect and cleanup in the return function (or guard with a flag) so it doesn’t re-register on every re-run. [2]

  • If you’re on newer GL JS, consider the Interactions API (map.addInteraction(id, ...)) which is keyed by an interaction id, making it easier to manage/replace/remove without accidental stacking. [3]

Sources:
[1] Mapbox GL JS “Events and event types” (Evented interface) https://docs.mapbox.com/mapbox-gl-js/api/events/
[2] Stack Overflow: “Double on click event with mapbox gl” (use once / off and named handlers) https://stackoverflow.com/questions/45663869/double-on-click-event-with-mapbox-gl
[3] Mapbox GL JS Interactions guide (Interactions API) https://docs.mapbox.com/mapbox-gl-js/guides/user-interactions/interactions/


🏁 Script executed:

cd frontend/src/components && rg "map\.on\(" MapComponent.jsx -A 2 -B 2

Repository: thejonathangu/tartanhacks

Length of output: 649


🏁 Script executed:

cd frontend/src/components && rg "useEffect.*=>.*\{" MapComponent.jsx -A 5 | head -80

Repository: thejonathangu/tartanhacks

Length of output: 1435


🏁 Script executed:

cd frontend/src/components && grep -n "return.*=>" MapComponent.jsx | head -20

Repository: thejonathangu/tartanhacks

Length of output: 103


🏁 Script executed:

cd frontend/src/components && sed -n '195,215p' MapComponent.jsx

Repository: thejonathangu/tartanhacks

Length of output: 561


Prevent duplicate event handlers on uploaded layers.
Each uploadedBookLocations update re-attaches handlers without cleanup, causing them to stack and trigger repeatedly. Mapbox GL JS event handlers do not auto-deduplicate, so calling map.on() multiple times registers the listener again rather than replacing it. Since the handlers are inline arrow functions, they cannot be removed via map.off() without a stored reference.

Suggested fix
+  const uploadedHandlersRef = useRef(null);
+
   useEffect(() => {
     const map = mapRef.current;
     if (!map || !uploadedBookLocations) return;

+    if (!uploadedHandlersRef.current) {
+      uploadedHandlersRef.current = {
+        onClick: (e) => {
+          const m = mapRef.current;
+          if (!m) return;
+          const feature = e.features[0];
+          const coords = feature.geometry.coordinates.slice();
+          m.flyTo({ center: coords, zoom: 14, speed: 1.2, pitch: 45 });
+          if (onMarkerClickRef.current) onMarkerClickRef.current(feature);
+        },
+        onEnter: () => {
+          const m = mapRef.current;
+          if (m) m.getCanvas().style.cursor = "pointer";
+        },
+        onLeave: () => {
+          const m = mapRef.current;
+          if (m) m.getCanvas().style.cursor = "";
+        },
+      };
+    }
+
     const addUploadedLayer = () => {
-      map.on("click", "uploaded-markers", (e) => {
-        const feature = e.features[0];
-        const coords = feature.geometry.coordinates.slice();
-        map.flyTo({ center: coords, zoom: 14, speed: 1.2, pitch: 45 });
-        if (onMarkerClickRef.current) onMarkerClickRef.current(feature);
-      });
-
-      map.on("mouseenter", "uploaded-markers", () => {
-        map.getCanvas().style.cursor = "pointer";
-      });
-      map.on("mouseleave", "uploaded-markers", () => {
-        map.getCanvas().style.cursor = "";
-      });
+      const { onClick, onEnter, onLeave } = uploadedHandlersRef.current;
+      map.off("click", "uploaded-markers", onClick);
+      map.off("mouseenter", "uploaded-markers", onEnter);
+      map.off("mouseleave", "uploaded-markers", onLeave);
+      map.on("click", "uploaded-markers", onClick);
+      map.on("mouseenter", "uploaded-markers", onEnter);
+      map.on("mouseleave", "uploaded-markers", onLeave);
🤖 Prompt for AI Agents
In `@frontend/src/components/MapComponent.jsx` around lines 286 - 367, The
uploaded-markers event handlers are re-attached on every uploadedBookLocations
update causing duplicate listeners; modify the useEffect that defines
addUploadedLayer to register named handler functions (or store references) for
the "click", "mouseenter", and "mouseleave" events on "uploaded-markers" and
remove them in the useEffect cleanup (use map.off for "uploaded-markers" with
the same handler references) and also remove the "uploaded-markers" and
"uploaded-labels" layers and "uploaded-points" source in that cleanup; ensure
addUploadedLayer still adds layers/sources only once (use
map.isStyleLoaded()/map.once as currently) and that the cleanup also unregisters
the style.load listener if you attach one.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +370 to +423
// ── Popup rendering ──
useEffect(() => {
if (!popupContent || !mapRef.current) return;
if (popupRef.current) popupRef.current.remove();

const { geometry, properties, deepContext } = popupContent;
const coords = geometry.coordinates;
const [lng, lat] = coords;

const accentColor =
properties.era === "1940s"
? "#e6b800"
: properties.era === "1920s"
? "#ff6b6b"
: "#4ecdc4";

const streetViewUrl = `https://maps.googleapis.com/maps/api/streetview?size=600x300&location=${lat},${lng}&fov=90&heading=235&pitch=10&key=${GOOGLE_API_KEY}`;

const html = `
<div style="max-width:320px;font-family:system-ui,sans-serif;color:#1a1a2e;">
<div style="position:relative;margin:-10px -10px 8px -10px;overflow:hidden;border-radius:8px 8px 0 0;">
<img src="${streetViewUrl}" alt="Street View"
style="width:100%;height:160px;object-fit:cover;display:block;"
onerror="this.parentElement.style.display='none'" />
<div style="position:absolute;top:8px;right:8px;background:${accentColor};
padding:2px 8px;border-radius:12px;font-size:10px;font-weight:700;color:#000;">
${properties.era}
</div>
</div>
<h3 style="margin:0 0 4px;font-size:14px;">${properties.title}</h3>
<p style="color:#888;margin:0 0 8px;font-size:11px;">${properties.book} · ${properties.year}</p>
<blockquote style="border-left:3px solid ${accentColor};padding-left:8px;margin:0 0 8px;
font-style:italic;font-size:12px;color:#444;line-height:1.5;">
"${properties.quote}"
</blockquote>
${
deepContext?.historical_context
? `
<p style="font-size:11px;color:#555;margin:0;line-height:1.4;">
<strong style="color:${accentColor};">🏛</strong> ${deepContext.historical_context}
</p>
`
: `<p style="font-size:11px;color:#999;margin:0;">Click explores this via MCP agents →</p>`
}
</div>
`;

popupRef.current = new mapboxgl.Popup({
closeOnClick: true,
maxWidth: "340px",
})
.setLngLat(coords)
.setHTML(html)
.addTo(mapRef.current);

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "MapComponent.jsx" -type f

Repository: thejonathangu/tartanhacks

Length of output: 111


🏁 Script executed:

sed -n '370,423p' ./frontend/src/components/MapComponent.jsx | cat -n

Repository: thejonathangu/tartanhacks

Length of output: 2744


🏁 Script executed:

grep -n "setPopupContent\|popupContent" ./frontend/src/components/MapComponent.jsx | head -20

Repository: thejonathangu/tartanhacks

Length of output: 238


🏁 Script executed:

sed -n '1,100p' ./frontend/src/components/MapComponent.jsx | cat -n

Repository: thejonathangu/tartanhacks

Length of output: 3727


🏁 Script executed:

grep -r "popupContent" ./frontend/src --include="*.jsx" --include="*.js" -n | grep -v "MapComponent.jsx"

Repository: thejonathangu/tartanhacks

Length of output: 219


🏁 Script executed:

sed -n '120,200p' ./frontend/src/App.jsx | cat -n

Repository: thejonathangu/tartanhacks

Length of output: 3286


🏁 Script executed:

grep -n "fetchConductorOrchestrate" ./frontend/src/App.jsx | head -5

Repository: thejonathangu/tartanhacks

Length of output: 300


🏁 Script executed:

grep -n "literaryGeoJSON" ./frontend/src/components/MapComponent.jsx

Repository: thejonathangu/tartanhacks

Length of output: 278


🏁 Script executed:

find ./frontend/src/data -name "literaryPoints*" -type f

Repository: thejonathangu/tartanhacks

Length of output: 106


🏁 Script executed:

head -100 ./frontend/src/data/literaryPoints.js | cat -n

Repository: thejonathangu/tartanhacks

Length of output: 4506


🏁 Script executed:

grep -n "uploadedBookLocations" ./frontend/src/App.jsx | head -10

Repository: thejonathangu/tartanhacks

Length of output: 209


🏁 Script executed:

sed -n '135,250p' ./frontend/src/App.jsx | cat -n

Repository: thejonathangu/tartanhacks

Length of output: 4700


🏁 Script executed:

grep -rn "setUploadedBookLocations" ./frontend/src --include="*.jsx" --include="*.js"

Repository: thejonathangu/tartanhacks

Length of output: 261


🏁 Script executed:

sed -n '450,470p' ./frontend/src/App.jsx | cat -n

Repository: thejonathangu/tartanhacks

Length of output: 860


🏁 Script executed:

find ./frontend/src -name "BookUpload*" -type f

Repository: thejonathangu/tartanhacks

Length of output: 109


🏁 Script executed:

grep -n "onLocationsExtracted" ./frontend/src/components/BookUpload.jsx | head -10

Repository: thejonathangu/tartanhacks

Length of output: 206


Sanitize popup HTML content to prevent XSS.

The code interpolates user data directly into HTML via setHTML(): API responses (deepContext.historical_context) and uploaded book data can inject malicious scripts. Apply HTML escaping to all interpolated values—including properties.era on line 27, which is also unescaped.

🛠️ Suggested fix (escape values before interpolation)
+const escapeHtml = (value = "") =>
+  String(value).replace(/[&<>"']/g, (c) => ({
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': "&quot;",
+    "'": "&#39;",
+  }[c]));
@@
-    const html = `
+    const html = `
       <div style="max-width:320px;font-family:system-ui,sans-serif;color:`#1a1a2e`;">
@@
+            ${escapeHtml(properties.era)}
@@
-        <h3 style="margin:0 0 4px;font-size:14px;">${properties.title}</h3>
-        <p style="color:`#888`;margin:0 0 8px;font-size:11px;">${properties.book} · ${properties.year}</p>
+        <h3 style="margin:0 0 4px;font-size:14px;">${escapeHtml(properties.title)}</h3>
+        <p style="color:`#888`;margin:0 0 8px;font-size:11px;">${escapeHtml(properties.book)} · ${escapeHtml(properties.year)}</p>
@@
-          "${properties.quote}"
+          "${escapeHtml(properties.quote)}"
@@
-            <strong style="color:${accentColor};">🏛</strong> ${deepContext.historical_context}
+            <strong style="color:${accentColor};">🏛</strong> ${escapeHtml(deepContext.historical_context)}
🤖 Prompt for AI Agents
In `@frontend/src/components/MapComponent.jsx` around lines 370 - 423, The popup
HTML is built by interpolating unescaped user/API data (popupContent ->
properties.*, deepContext.historical_context) then passed to
mapboxgl.Popup.setHTML, which allows XSS; fix by sanitizing all interpolated
values (properties.era, properties.title, properties.book, properties.year,
properties.quote, deepContext.historical_context, and any coords-derived
strings) before interpolation — either implement a small escapeHtml utility and
call it for each inserted value, or instead construct the popup with DOM nodes
and use setDOMContent to assign text via textContent (avoid setHTML with raw
strings); update the popup creation (where popupRef.current is assigned and
setHTML is called) to use the chosen safe approach and ensure accentColor
remains derived from a whitelist of expected era values rather than raw input.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +75 to +111
action = body.get("action")

# ── Book search shortcut — delegates to LibrarianAgent only ──
if action == "search":
query = body.get("query")
if not query:
return JsonResponse({"error": "query is required for search action"}, status=400)
limit = body.get("limit", 10)
t0 = time.perf_counter()
try:
result = _librarian_search(query, limit=limit)
elapsed = round((time.perf_counter() - t0) * 1000)
total = round((time.perf_counter() - t_start) * 1000)
return JsonResponse({
"librarian": result,
"timeline": [{
"agent": "LibrarianAgent",
"tool": "search_books",
"status": "success",
"elapsed_ms": elapsed,
}],
"total_ms": total,
})
except Exception as exc:
elapsed = round((time.perf_counter() - t0) * 1000)
total = round((time.perf_counter() - t_start) * 1000)
return JsonResponse({
"error": str(exc),
"timeline": [{
"agent": "LibrarianAgent",
"tool": "search_books",
"status": "error",
"elapsed_ms": elapsed,
"error": str(exc),
}],
"total_ms": total,
}, status=502)

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "conductor.py" -type f | head -20

Repository: thejonathangu/tartanhacks

Length of output: 100


🏁 Script executed:

git ls-files | grep -E "(conductor|librarian)" | head -20

Repository: thejonathangu/tartanhacks

Length of output: 223


🏁 Script executed:

wc -l ./mcp-servers/core/conductor.py

Repository: thejonathangu/tartanhacks

Length of output: 104


🏁 Script executed:

cat -n ./mcp-servers/core/conductor.py | head -150

Repository: thejonathangu/tartanhacks

Length of output: 6703


🏁 Script executed:

cat -n ./mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 4072


🏁 Script executed:

grep -n "import" ./mcp-servers/core/conductor.py

Repository: thejonathangu/tartanhacks

Length of output: 622


Validate search inputs and narrow exception handling.

The _librarian_search function validates and strips the query internally, but conductor.py should validate inputs earlier. More critically, limit is never validated—it can be a string, float, or out of bounds, causing unexpected behavior downstream. The blanket Exception catch also masks the distinction between validation errors (which should return 400) and network/API errors (502): _librarian_search raises ValueError for invalid queries, which should map to 400, not 502.

🛠️ Suggested fix
+import httpx
@@
-    if action == "search":
-        query = body.get("query")
-        if not query:
-            return JsonResponse({"error": "query is required for search action"}, status=400)
-        limit = body.get("limit", 10)
+    if action == "search":
+        query = (body.get("query") or "").strip()
+        if not query:
+            return JsonResponse({"error": "query is required for search action"}, status=400)
+        try:
+            limit = int(body.get("limit", 10))
+        except (TypeError, ValueError):
+            return JsonResponse({"error": "limit must be an integer"}, status=400)
+        limit = max(1, min(limit, 25))
@@
-        try:
-            result = _librarian_search(query, limit=limit)
+        try:
+            result = _librarian_search(query, limit=limit)
@@
-        except Exception as exc:
+        except ValueError as exc:
+            elapsed = round((time.perf_counter() - t0) * 1000)
+            total = round((time.perf_counter() - t_start) * 1000)
+            return JsonResponse({
+                "error": str(exc),
+                "timeline": [{
+                    "agent": "LibrarianAgent",
+                    "tool": "search_books",
+                    "status": "error",
+                    "elapsed_ms": elapsed,
+                    "error": str(exc),
+                }],
+                "total_ms": total,
+            }, status=400)
+        except (httpx.HTTPStatusError, httpx.RequestError) as exc:
             elapsed = round((time.perf_counter() - t0) * 1000)
             total = round((time.perf_counter() - t_start) * 1000)
             return JsonResponse({
                 "error": str(exc),
                 "timeline": [{
                     "agent": "LibrarianAgent",
                     "tool": "search_books",
                     "status": "error",
                     "elapsed_ms": elapsed,
                     "error": str(exc),
                 }],
                 "total_ms": total,
             }, status=502)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 98-98: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@mcp-servers/core/conductor.py` around lines 75 - 111, Ensure conductor.py
validates and normalizes the search inputs before calling _librarian_search:
strip and reject empty query (use the existing action/query variables) and
validate/convert limit to an int with sensible bounds (e.g., default 10, clamp
to 1..100 or similar); return JsonResponse with status 400 for input validation
failures. Narrow the exception handling around the _librarian_search call: catch
ValueError specifically and return a 400 error with the validation message, and
catch other exceptions (generic) as before returning 502; preserve timing
calculations (t0, t_start) and include the same timeline structure but use the
appropriate status/error codes per error type.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +81 to +88
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)

query = body.get("query")
if not query:
return JsonResponse({"error": "query is required"}, status=400)

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the actual file
find . -type f -name "views.py" | grep -i librarian

Repository: thejonathangu/tartanhacks

Length of output: 101


🏁 Script executed:

# Understand Python's json.loads() behavior with different JSON types
python3 << 'EOF'
import json

# Test 1: JSON array
array_json = '["item1", "item2"]'
array_result = json.loads(array_json)
print(f"Array JSON result type: {type(array_result)}")
print(f"Array JSON result: {array_result}")

# Test 2: JSON string
string_json = '"hello"'
string_result = json.loads(string_json)
print(f"String JSON result type: {type(string_result)}")
print(f"String JSON result: {string_result}")

# Test 3: JSON number
number_json = '123'
number_result = json.loads(number_json)
print(f"Number JSON result type: {type(number_result)}")
print(f"Number JSON result: {number_result}")

# Test 4: JSON null
null_json = 'null'
null_result = json.loads(null_json)
print(f"Null JSON result type: {type(null_result)}")
print(f"Null JSON result: {null_result}")

# Test 5: What happens when calling .get() on non-dict types?
print("\n--- Testing .get() on non-dict types ---")
test_cases = [array_result, string_result, number_result, null_result]
for i, test_value in enumerate(test_cases):
    try:
        result = test_value.get("query")
        print(f"Test {i+1} (.get() succeeded): {result}")
    except AttributeError as e:
        print(f"Test {i+1} (.get() failed with AttributeError): {type(test_value).__name__} has no attribute 'get'")
    except Exception as e:
        print(f"Test {i+1} (.get() failed with {type(e).__name__}): {e}")
EOF

Repository: thejonathangu/tartanhacks

Length of output: 670


🏁 Script executed:

cat -n ./mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 4072


Add type guard to ensure JSON body is an object.

If the request payload is a valid JSON array, string, number, or null, body.get(...) on line 86 (and line 90) will raise an uncaught AttributeError, resulting in a 500 error instead of the intended 400.

🛡️ Suggested guard
     try:
         body = json.loads(request.body)
     except json.JSONDecodeError:
         return JsonResponse({"error": "Invalid JSON"}, status=400)
 
+    if not isinstance(body, dict):
+        return JsonResponse({"error": "JSON object required"}, status=400)
+
     query = body.get("query")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
query = body.get("query")
if not query:
return JsonResponse({"error": "query is required"}, status=400)
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON"}, status=400)
if not isinstance(body, dict):
return JsonResponse({"error": "JSON object required"}, status=400)
query = body.get("query")
if not query:
return JsonResponse({"error": "query is required"}, status=400)
🤖 Prompt for AI Agents
In `@mcp-servers/librarian/views.py` around lines 81 - 88, The JSON-parsing block
uses json.loads(request.body) and then calls body.get("query") which will raise
AttributeError if the parsed body is not an object; add a type guard after
parsing to verify isinstance(body, dict) (or that body has .get) and return
JsonResponse({"error": "JSON object required"}, status=400) if it is not, so
subsequent uses of body.get(...) are safe; update the code around the
json.loads(...) / body variable and the JsonResponse error paths accordingly.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines +90 to +94
limit = body.get("limit", 10)

try:
result = _librarian_search(query, limit=limit)
except ValueError as e:

@coderabbitai coderabbitai Bot Feb 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the file and check its size
find . -name "views.py" -path "*/librarian/*" | head -5

Repository: thejonathangu/tartanhacks

Length of output: 101


🏁 Script executed:

# Read the file around lines 90-94 to understand the context
wc -l mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 103


🏁 Script executed:

# Get the relevant section with context
sed -n '80,105p' mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 875


🏁 Script executed:

# Find the _librarian_search function definition
grep -n "_librarian_search" mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 189


🏁 Script executed:

# Read the function definition
sed -n '1,50p' mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 1522


🏁 Script executed:

# Check the full _librarian_search implementation
sed -n '24,70p' mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 1716


🏁 Script executed:

# Search for any validation or limit bounds in the codebase
rg "limit" mcp-servers/librarian/ -A 2 -B 2

Repository: thejonathangu/tartanhacks

Length of output: 1467


🏁 Script executed:

# Check if there are any other files that might document Open Library API limits
find . -type f -name "*.md" -o -name "*.txt" -o -name "*.py" | xargs grep -l "Open Library" 2>/dev/null | head -10

Repository: thejonathangu/tartanhacks

Length of output: 129


🏁 Script executed:

# Check for any constants or configuration related to limits
rg "limit.*[0-9]+" mcp-servers/librarian/ -B 2 -A 2

Repository: thejonathangu/tartanhacks

Length of output: 909


🏁 Script executed:

# Check the entire file to see if there's documentation about API constraints
cat -n mcp-servers/librarian/views.py

Repository: thejonathangu/tartanhacks

Length of output: 4072


🌐 Web query:

Open Library API search limit parameter bounds constraints

💡 Result:

For Open Library’s Search API (GET https://openlibrary.org/search.json), the docs describe limit only as a pagination page-size parameter (usable with either offset or page) but do not document any minimum/maximum bounds or a hard cap for limit. [1]

Related Open Library endpoints do document caps (useful as a reference point, but they are different APIs), e.g. the Query API (/query.json) explicitly caps limit at 1000 “for performance reasons”. [2]

Sources:
[1] Open Library Developer Center — Search API (offset/limit, page/limit, no max stated) (openlibrary.org)
[2] Open Library Developer Center — RESTful API / Query API (limit max 1000) (openlibrary.org)

Citations:


Validate and bound limit before calling Open Library.

The limit parameter is extracted from the JSON body without type checking or bounds validation. A client can send a non-integer (e.g., "limit": "abc") or an unbounded value (e.g., "limit": 10000000), causing type errors or excessive upstream requests. Add validation to ensure limit is an integer within a reasonable range.

🚧 Suggested validation
-    limit = body.get("limit", 10)
+    limit = body.get("limit", 10)
+    try:
+        limit = int(limit)
+    except (TypeError, ValueError):
+        return JsonResponse({"error": "limit must be an integer"}, status=400)
+    if not (1 <= limit <= 50):
+        return JsonResponse({"error": "limit must be between 1 and 50"}, status=400)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
limit = body.get("limit", 10)
try:
result = _librarian_search(query, limit=limit)
except ValueError as e:
limit = body.get("limit", 10)
try:
limit = int(limit)
except (TypeError, ValueError):
return JsonResponse({"error": "limit must be an integer"}, status=400)
if not (1 <= limit <= 50):
return JsonResponse({"error": "limit must be between 1 and 50"}, status=400)
try:
result = _librarian_search(query, limit=limit)
except ValueError as e:
🤖 Prompt for AI Agents
In `@mcp-servers/librarian/views.py` around lines 90 - 94, The request handler
currently reads limit = body.get("limit", 10) and passes it directly to
_librarian_search; validate and bound it before that call: ensure the incoming
limit is an integer (coerce numeric strings or reject non-numeric), enforce a
sane range (e.g., min 1, max 100) and clamp or return a 400 Bad Request on
invalid input, and then pass the validated/clamped limit to _librarian_search;
update the handler's validation logic around the limit variable and add explicit
error handling for bad types/ranges.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot resolve this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #19, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #20, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #21, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #22, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #23, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #24, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #25, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #26, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #27, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #28, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #29, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #30, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #31, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #32, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #33, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI commented Feb 7, 2026

Copy link
Copy Markdown

@thejonathangu I've opened a new pull request, #34, to work on those changes. Once the pull request is ready, I'll request review from you.

@thejonathangu thejonathangu merged commit 77c5f84 into main Feb 7, 2026
8 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants