KG to Website: Frontend Integration Guide
Audience: the engineer wiring the knowledge graph into the maps and search on mgc-frontend. This is the website side only. No database/schema/loader content here.
1. TL;DR
- Every KG call goes through the existing same-origin proxy:
GET /api/mgc/kg/*on the Next.js app, defined inapp/api/mgc/[...path]/route.ts.kg/is already in its allowlist. You do not need to touch the proxy. - The hooks and components already exist, on branch
feat/kg-relationships-panel(per the branch's own git log; the PR number quoted to me was #175 but I could not independently confirm that number from the repo, flag if it matters). - The map <-> KG join is deterministic: every map feature already carries
its
/vizrow id (e.g.port:34,plant:<hash>,investment:<n>). No new ids to invent, no fuzzy matching. - Your job is wiring: each map's click handler ->
deriveMapPoint(or acountry:<iso3>string) ->RelationshipsPanel. Plus placing the search box (AskMgcBox). - The one hard rule: a layer only resolves against the KG if it serves
ids from the
/vizrow-id scheme (kg.map_linkis keyed on(dataset, row_id)=(viz:<family>, <that row's /viz id>)). If a layer's data comes from anywhere else, with a different id namespace, it will silently resolve to nothing until repointed. See section 5.
2. Where to call the KG
The proxy at app/api/mgc/[...path]/route.ts allowlists a set of endpoint
prefixes (viz/, country/, energy/, ... and kg/). Any client-side
fetch('/api/mgc/kg/...') is forwarded server-side to the real mgc-api at
${MGC_API_BASE_URL}/kg/..., with the deployed API's HTTP Basic Auth
credentials attached. This is the same proxy and the same env vars the
/viz/* map data calls already use: there is no separate KG proxy and no
new server-side work required.
Env vars the deployed app needs (server-side, no NEXT_PUBLIC_ prefix,
read in src/lib/api/mgc-api/client.ts):
| Var | Purpose |
|---|---|
MGC_API_BASE_URL |
Base URL of the deployed mgc-api (e.g. https://api.mapglobalchina.com). Falls back to that same production host with a logged warning if unset; do not rely on the fallback. |
MGC_API_USERNAME / MGC_API_PASSWORD |
HTTP Basic Auth for the deployed API (it sits behind nginx Basic Auth). Optional only on hosts with no auth. |
There is also a dev-only override, MGC_KG_API_BASE_URL, handled directly
in the proxy route: when set, every kg/* request is forwarded to that URL
instead of the live mgc-api, while everything else (viz/, country/,
etc.) still goes to the live API. It exists purely so the panel can be
developed against the local reference KG service before the real /kg/*
endpoints are live in production (see .env.local, currently pointed at
http://localhost:18010). It is inert when unset, but the code comment is
explicit that it must not ship in the PR: do not deploy with it set.
3. The hooks and components you reuse
All hooks live in src/hooks/kg/ and are re-exported from
src/hooks/kg/index.ts (import { useKgResolve, ... } from '@/hooks/kg').
Every hook follows the same shape: { data, loading, error } (or data,
resolvedId, loading, error for resolve), backed by React Query, disabled
until its required argument is present.
| Hook / component | File | Signature | What it does |
|---|---|---|---|
useKgResolve |
src/hooks/kg/useKgResolve.ts |
useKgResolve({dataset, rowId}, {primaryOnly?}) -> {data, resolvedId, loading, error} |
Resolves a (dataset, row_id) map point to its KG node(s) via GET /kg/resolve?dataset=&row_id=. resolvedId is entity_id ?? node_id of the first hit, or undefined if the point has no KG link (not an error). This is the "first hop" from a map click into the graph. |
useKgEntity |
src/hooks/kg/useKgEntity.ts |
useKgEntity(entityId?, {hops?, relationType?, tier?, limit?}) -> {data, loading, error} |
Neighborhood bundle for one entity: {center, edges, neighbors} via GET /kg/entity/{id}. Feeds EntityNetworkGraph. |
useKgRelations |
src/hooks/kg/useKgRelations.ts |
useKgRelations(entityId?, {relationType?, direction?, tier?, minConfidence?, limit?, offset?}) -> {data, loading, error} |
Flat list of KG edges touching an entity, each with provenance, via GET /kg/entity/{id}/relations. |
useKgClaims |
src/hooks/kg/useKgClaims.ts |
useKgClaims(aboutId?, {stance?, minConfidence?, limit?, offset?}) -> {data, loading, error} |
Stance-bearing claims about an entity via GET /kg/claims?about=. |
useKgEvents |
src/hooks/kg/useKgEvents.ts |
useKgEvents(participantId?, {eventType?, yearFrom?, yearTo?, limit?, offset?}) -> {data, loading, error} |
Events the entity participated in via GET /kg/events?participant=. |
useKgAsk |
src/hooks/kg/useKgAsk.ts |
useKgAsk(question?, {limit?}) -> {data, loading, error} |
The "Ask MGC" natural-language endpoint: GET /kg/ask?q=&limit=. Returns an interpretation line, matched entities, ranked cited results, and a focused graph. |
useKgSearch |
src/hooks/kg/useKgSearch.ts |
useKgSearch(q?, {entityType?, limit?}) -> {data, loading, error} |
Entity name lookup via GET /kg/search?q=. Exists and works but is not currently mounted in any component; it's there if you want to add a typeahead. |
useKgPath |
src/hooks/kg/useKgPath.ts |
useKgPath(fromId?, toId?, {maxHops?, minConfidence?}) -> {data, loading, error} |
Bounded shortest path between two entities via GET /kg/path. Not currently used by any component either, available if a future feature needs it. |
deriveMapPoint |
src/components/maps/shared/kgMapPoint.ts |
deriveMapPoint(clicked: {id}, family?: MapPointFamily) -> {dataset, rowId} \| null |
Turns a clicked feature's /viz id into the {dataset, rowId} shape RelationshipsPanel's mapPoint prop (and useKgResolve) expect. dataset is built as `viz:${family}`. Pass family explicitly whenever you know it (recommended); it will only infer the family from the id's prefix (port:, plant:, asset:, investment:, bri-contract:, state-visit:) if you omit it. |
RelationshipsPanel |
src/components/maps/shared/RelationshipsPanel.tsx |
<RelationshipsPanel entityId={string \| null} entityLabel={string} onClose={() => void} onFocusEntity?={(id) => void} accentColor? width?=440 mapPoint?={{dataset, rowId}} /> |
The sidebar/drawer. Two entry modes: pass mapPoint for a per-point layer (it resolves via useKgResolve internally, then falls through to the entity hooks), or pass entityId directly for a choropleth/country selection. mapPoint takes precedence over entityId when both are present. Renders Network / Relations / Claims / Events tabs, each using CitationBlock (also exported from this file) for provenance. Returns null (renders nothing) when neither entityId nor mapPoint is set. |
EntityNetworkGraph |
src/components/charts/EntityNetworkGraph.tsx |
<EntityNetworkGraph nodes={KgGraphNode[]} links={KgGraphLink[]} width={number} height?=400 focusId? onNodeClick? onNodeHover? /> |
Pure presentational force-directed graph (d3-force, settles synchronously, plain SVG). No data fetching: RelationshipsPanel's Network tab and AskMgcAnswer's graph both build nodes/links from their own query data and pass them in. |
AskMgcBox |
src/components/kg/AskMgcBox.tsx |
<AskMgcBox placeholder? onFocusEntity? graphWidth? elevated?=true /> |
The search input. Calls useKgAsk internally on submit and renders AskMgcAnswer below the field. elevated=false drops its own card surface for when the caller already supplies one (e.g. a map overlay Paper). |
AskMgcAnswer |
src/components/kg/AskMgcAnswer.tsx |
<AskMgcAnswer answer={KgAskAnswer \| null} loading? error? onFocusEntity? graphWidth?=480 /> |
Presentational renderer for one /kg/ask answer: interpretation heading, fallback note, matched-entity chips, cited result rows (with CitationBlock), and a collapsed-by-default focused EntityNetworkGraph. |
4. Joining KG onto existing map data
This is the core mechanic. Every feature on every MGC map already carries
its /viz row id as the feature id (examples from kgMapPoint.ts:
port:34 for ports, plant:<hash> for energy plants, investment:<n>,
asset:<n> for infrastructure, bri-contract:<n>, state-visit:<n>).
The backend's kg.map_link table is keyed on (viz:<family>, <that id>),
so joining a map feature to the graph is just building that pair and
resolving it.
Pattern A: per-point layer (a clicked marker/dot). On the layer's
click handler, call deriveMapPoint(feature, family) and pass the result
as mapPoint to RelationshipsPanel. Real example, the energy map,
app/maps/energy/page.tsx:
// on a project point click
const handleProjectClick = useCallback((project: EnergyProject) => {
const point = deriveMapPoint({ id: project.id }, 'energy-projects')
if (!point) return
setMapPointLabel(project.plant_name || project.unit_name || 'Energy project')
setMapPoint(point)
}, [])
// ...
{!selectedCountryData && mapPoint && (
<RelationshipsPanel
entityId={null}
entityLabel={mapPointLabel}
mapPoint={mapPoint}
onClose={() => setMapPoint(null)}
onFocusEntity={(id) => store.set('selectedCountryIso3', id)}
/>
)}
Here project.id is already the /viz row id (plant:<hash>) as it comes
straight off useEnergyData's /viz/energy-projects rows
(src/hooks/energy/useEnergyData.ts), so no id translation is needed
before calling deriveMapPoint.
Pattern B: country choropleth. Pass entityId={ country:${iso3} }
directly, no deriveMapPoint/useKgResolve hop needed, because the
country nodes are addressed by ISO3 without going through kg.map_link.
Real example, the investment map, app/maps/investment/page.tsx:
// KG entity id fed to RelationshipsPanel: the clicked neighbor node when
// set, otherwise the selected country's KG node (`country:<iso3>`).
const relationshipsEntityId = relationshipsFocusId
?? (store.selectedCountryIso3 ? `country:${store.selectedCountryIso3}` : null)
// ...
<RelationshipsPanel
entityId={relationshipsEntityId}
entityLabel={selectedCountryData.name}
onClose={() => setShowRelationships(false)}
onFocusEntity={(id) => setRelationshipsFocusId(id)}
/>
The country: prefix is required: a bare ISO3 string will not resolve
(see section 5).
5. The hard rule and gotchas
- A layer resolves only if it serves the
/vizid scheme. If a layer's markers come from a different source with a different id namespace,kg.map_linkhas nothing to match against and every point will resolve to an empty panel. Historically the energy map's project points sourced their id from the Platform API'scgp_id(values likeBY.001), which does not matchkg.map_link'sviz:energy-projectsids; that has since been fixed by switching to the/viz/energy-projectsfeed's ownidfield (plant:<hash>, confirmed insrc/hooks/energy/useEnergyData.tsand the click handler above). If you add or touch a layer that is not already backed by/viz/<family>, it needs the same repoint before KG wiring will do anything. - The
country:prefix is required for choropleth/country entity ids. A bare ISO3 (e.g."USA") will not resolve; it must be"country:USA". Both the investment and energy maps now build this the same way: arelationshipsFocusIdstate for a focused neighbor node, falling back tostore.selectedCountryIso3 ?`country:${store.selectedCountryIso3}`: null(seeapp/maps/investment/page.tsxandapp/maps/energy/page.tsx). Copy that pattern for any new country panel; do not feed a focused node id back intoselectedCountryIso3(keep the two states separate). - A point with no KG connections is not an error.
RelationshipsPaneldegrades to each tab's own clean empty state ("No relationships to display.","No known relations.", etc.) whenuseKgResolvefinds nothing or the entity has no edges/claims/events. Wiring a thin or partially-covered layer is safe: it just shows an empty panel for unlinked points, it does not throw or break the map.
6. Search ("Ask MGC")
Mount AskMgcBox wherever a search entry point belongs: nav, hero, or a
map overlay. It is already mounted in two places you can copy:
/research(app/research/page.tsx): a plain standalone<AskMgcBox />inside aContainer.- The energy map (
app/maps/energy/page.tsx): a collapsible top-right overlay, anIconButtontoggles aPapercontaining<AskMgcBox graphWidth={320} elevated={false} />(elevated={false}because the surroundingPaperalready supplies the card surface).
AskMgcBox calls useKgAsk, which hits GET /api/mgc/kg/ask?q=&limit=.
There is also GET /api/mgc/kg/search?q= (via useKgSearch) for plain
entity-name autocomplete, if you want a typeahead; it is not wired into
AskMgcBox today.
Important caveat: /kg/ask (and the relevance filter it depends on,
which hides low-information "hub" connections like a project's own country)
currently exists only in the local reference service,
kg-demo/local_kg_api.py (@app.get("/kg/ask")). It has not been
confirmed present in the deployed mgc-api. That means search wiring works
today against the local KG service (MGC_KG_API_BASE_URL pointed at
local_kg_api.py) but will not work against the deployed API until
/kg/ask is ported to production. That porting is the API person's task,
not yours, but it blocks your search feature going live: flag it as a
dependency rather than assuming it will just work once you set
MGC_API_BASE_URL to the production host.
7. The bidirectional UX (optional, next step)
"View connections" (map point -> graph) is built: click a feature ->
deriveMapPoint -> useKgResolve (inside RelationshipsPanel) -> the
panel's tabs.
"Show on map" (a graph/panel node -> its map location) is the reverse
direction and is not built yet. It would need a small addition: resolve
a KG node id back to its (dataset, row_id) and fly the map to that
feature's coordinates. Treat this as a follow-up enhancement, not a v1
requirement; nothing in the checklist below depends on it.
8. Your checklist
- Confirm the deployed mgc-api's
/kgbase URL and Basic Auth creds are set in the frontend's env (MGC_API_BASE_URL,MGC_API_USERNAME,MGC_API_PASSWORD), same vars the/viz/*map data already uses, no new proxy config needed. Make sureMGC_KG_API_BASE_URLis not set in the deployed environment (it's the local-dev-only override). - For each map: attach the feature's click handler to
deriveMapPoint(feature, family)(per-point layers) or build`country:${iso3}`(choropleths), and mountRelationshipsPanelwithmapPoint/entityIdrespectively, copying the energy map's or the investment map's pattern from section 4. - Repoint any layer whose feature ids are not already
/viz/<family>ids before wiring it (see section 5's gotcha). - Place
AskMgcBoxin the nav, a hero, or a map overlay, copying the/researchpage or the energy map's overlay pattern from section 6. - Coordinate with whoever owns the backend to get
/kg/askand its hub relevance filter deployed to production mgc-api. Your search feature is blocked on that endpoint existing outside the local reference service.
Everything above is proven and working today against the local reference
/kg service (kg-demo/local_kg_api.py). Going live is these wiring
steps, plus the deployed API actually serving /kg/* (including /kg/ask).