Proof of Concept. V2.0
A node-based visual workflow editor for federating UK Arts & Humanities research data services. Built as part of the UKRI/AHRC Federation of Compute and Infrastructures programme. Conceptualised by Neil Jakeman, King's Digital Lab.
Drag nodes onto a canvas, connect them in any order, and run federated searches across multiple heritage data services simultaneously. Records from different services are normalised to a common schema, can be filtered and transformed, reconciled against Wikidata authorities, enriched with AI inference, and exported as CSV, JSON, or GeoJSON. Local document folders and images can be analysed using KCL's hosted inference API or a locally-running LLM via Ollama. IIIF manifests can be browsed and annotated with region markers, with cropped image snippets passed directly to inference models. Workflows can be saved to disk and reloaded.
βΆ Watch the instructional video playlist β 6 videos covering the core workflow concepts and node types.
- Node.js v18 or later (nodejs.org)
- npm v9 or later (bundled with Node)
- A modern browser β Chrome or Edge 86+ required for the LocalFolderSourceNode (File System Access API) and for the native save dialog in SaveSearchNode (falls back to an automatic download on Firefox); all other nodes work in Firefox too
- KCL Inference API key β required only for KingsInference nodes; request access from King's Digital Lab
- Ollama running locally on port 11434 β required only for Ollama nodes (hidden from sidebar by default; use KingsInference for hosted inference)
- Puppeteer (installed automatically via
npm install) β required only for the Wait for JS rendering option in URLFetchNode; the headless browser runs inside the Vite dev server
git clone https://github.com/kingsdigitallab/nfcs-poc.git
cd nfcs-poc
npm install
npm run devOpen http://localhost:5174 in your browser.
The port is fixed at 5174 to avoid conflicts with other Vite projects.
Click πΎ Save in the top bar to download the current canvas as a workflow-YYYY-MM-DD.json file. This captures every node's position and configuration β query fields, filter/transform rules, prompts and settings, spatial bounding boxes, selectors, and ParamNode values.
Click π Load to restore a saved workflow. The canvas is replaced with the saved nodes and edges. All nodes start in an idle state with no results β run them again to repopulate data.
Note:
LocalFolderSourceNodefolder handles andLocalFileSourceNodefile handles cannot be serialised. After loading a workflow containing either, re-select the folder or file manually.
The sidebar groups nodes into collapsible categories. Click a group heading to collapse or expand it. Drag any node onto the canvas to add it.
| Node | Description |
|---|---|
| QuickStart | AI workflow planner. Describe a research question in plain English; the node calls the KCL inference API (arc:nexus) and returns a structured plan listing the most appropriate data service nodes, suggested queries, and commentary on source relevance. Click Instantiate workflow to place all recommended nodes on the canvas β search nodes, a SourceProfile for each source (or a shared Deduplicate β SourceProfile chain when multiple nodes of the same type are suggested), and TableOutput / MapOutput as appropriate. Wiring is created automatically. No handles β this node is a standalone planner. Requires a KCL API key. |
| Comment | A free-floating annotation label. Add a title and body text to document your workflow. No connectors by default. Select the node to reveal resize handles β drag any edge or corner to resize. Easter egg: click the title field 5 times within 1.5 seconds to unlock hidden input/output handles β useful for illustrating conceptual workflow gaps (e.g. marking a "data retrieval" step between metadata services and processing nodes). |
| Node | Description |
|---|---|
| Param | Holds a Text or Integer value. Connect its output handle to any search node input handle to inject a query parameter (e.g. wire a single query string into multiple search nodes at once). |
| Node | Description |
|---|---|
| QuickView | Inspect the full, untruncated value of any field across upstream records. Pick a field from the dropdown; navigate records with βΉ / βΊ buttons. Copy button per record. CSV/TSV values are paginated (50 rows per page). Large plain-text values are truncated at 50 000 chars. |
| ImageView | Resizable image viewer with two modes and a source output handle for piping images to inference nodes. Images mode β combobox field picker (type any dot-notation path, e.g. europeana.thumbnail; the dropdown suggests fields found in upstream records); a separate URL row accepts any public image URL directly (overrides the field picker when set). IIIF mode β accepts a IIIF Presentation API v2 or v3 manifest URL (pre-loaded with a Wellcome Collection example); navigates canvases with βΉ / βΊ buttons; zoom-tiered resolution requests via IIIF Image API. See ImageView and IIIF region annotation. |
| HTMLPreview | Renders fetchedHtml from upstream records in a sandboxed iframe with a lightweight readability stylesheet. Click any element in the preview to capture its CSS selector β the selector is sent back to the parent page and can be pasted directly into an HTMLExtract node. Two modes: Captured (reads stored fetchedHtml from the results store) and Live (fetches the URL field of each record in real time). |
| SourceProfile | Displays the authored schema profile for any connected data source, enriched with runtime field statistics computed from the actual upstream records. Shows: source coverage and limitations; a completeness bar (retrieved / total API results); a field table sorted by population rate with expandable sample values from real records; cross-source correspondence hints (which fields can be joined or compared across sources). An optional AI Narrative section (KCL API key + model required) builds a structured prompt from all schema and completeness data and streams a research-quality assessment β provide a research question for a focused response. Drag the output handle to pass records through to any downstream node. Defaults to arc:nano; max tokens configurable (default 16 384) under Advanced. |
All active search nodes share a fixture mode for offline and workshop use β see Offline fixtures below.
| Node | Service | Notes |
|---|---|---|
| ARIADNESearch | ARIADNE Infrastructure Portal | Pan-European archaeology data aggregator covering 40+ institutions across 23 countries. Direct browser fetch (permissive CORS). Inline fields: keyword query, limit, sort/order, and Fetch all results (paginates at 50 records/request). Collapsible Filters panel provides dropdowns for Resource type, Getty AAT subject, Native subject, Country, Data type, Period, and Contributor (ARIADNE aggregates records from the Archaeology Data Service and 40+ other institutions β filter by contributor to narrow to a specific provider). Citation metadata stamped on every record. |
| HSDSSearch | Heritage Science Data Service | UK heritage science data service aggregating records from Historic England, Historic Environment Scotland, Cadw (Wales), and other national bodies. Fetched via Vite proxy (no Cloudflare protection). Inline fields: keyword query, limit, sort/order, and Fetch all results (paginates at 50 records/request). Collapsible Filters panel: Resource type, Getty AAT subject, Native subject, Country (England/Scotland/Wales/Northern Ireland/Isle of Man), Data type, Period, and Contributor. Records include hsds.* namespace with landingPage, contributor, temporal, spatial, and subject arrays. Best for: scheduled monuments, listed buildings, UK historic environment records, built heritage, maritime archaeology. |
| BodleianSearch | Bodleian Digital Collections | Oxford's digital collections portal covering manuscripts, printed books, maps, photographs, coins, musical scores, and more. Inline fields: plain keyword query (e.g. psalter), limit (default 20), and sort order. Collapsible Filters panel: date range (from/to year), language (e.g. Latin), place of origin (e.g. England), completeness (fully digitised / partial), and musical notation presence. Records include bodleian.* namespace with shelfmark, date range, and IIIF manifest URL β connect output to ImageView in IIIF mode to browse manuscripts directly on the canvas. Fixture mode supported. |
| EuropeanaSearch | Europeana | Pan-European cultural heritage aggregator covering museums, galleries, libraries and archives. Direct browser fetch (permissive CORS). API key is pre-configured (π Configured); wire a Param node to the apiKey handle to override with your own key from apis.europeana.eu. Inline fields: query, limit (up to 1 000 records via cursor-based pagination). Filters: Type, Reusability, media only. Records include europeana.* namespace with thumbnail, shownAt (original institution URL), rights, provider, and completeness. |
| GBIFSearch | GBIF Occurrence API | Biodiversity specimens and observations. Direct browser fetch (permissive CORS). Inline fields: free-text q, scientificName, country, year, limit. |
| LLDSSearch | Literary & Linguistic Data Service | DSpace REST API. Results filtered client-side. Uses a 24-hour localStorage cache; a Use cache toggle controls fallback during outages. |
| MDSSearch | museumdata.uk | HTML scraper (no public JSON API). Capped at 200 records; amber β badge when the total exceeds the cap. |
| SMGSearch | Science Museum Group | Digital collection covering science, technology, medicine, and social history. Records include smg.manifest (IIIF) β connect to ImageView for object browsing. Fixture mode supported. |
| VASearch | Victoria & Albert Museum | V&A Collection API v2. Filters: images only, object type, year made from/to. Records include vam.manifest, vam.iiifImageBase, vam.thumbnail, vam.place, vam.objectType, vam.onDisplay. |
| SPARQLSearch | Wikidata Query Service + Getty Vocabularies | (Experimental) Structured queries over SPARQL endpoints without writing SPARQL: a Builder with live entity/property lookup (type names, pick from Wikidata's own autocomplete), a plain-English β¨ NL assist with hallucination-proof QID grounding, and a Raw SPARQL escape hatch. Endpoint dropdown: Wikidata (full features), Getty AAT/TGN/ULAN (raw mode), British Library BNB (greyed out until BL restores its linked-data service). Results carry _qid for WikidataEnrich/MergeByQID and coordinates for MapOutput. See SPARQL Search below. |
| LoadSavedSearch | Local filesystem | Loads a .nfcs.json file saved by SaveSearch, or any raw UnifiedRecord[] JSON array exported by Export. Displays full provenance metadata: saved date/time, source breakdown with per-service record counts, and the original search parameters in a collapsible panel. |
| LocalFileSource | Local filesystem | Parses a single CSV, TSV, XML, or image file selected via a standard file picker (all browsers). Auto-detects the delimiter. Cast numeric strings to numbers toggle converts coordinate strings to floats. |
| LocalFolderSource | Local filesystem | Reads files from a user-selected folder via the File System Access API. Supports PDF (text extraction), XML/TEI, plain text, images, Shapefiles, and GeoJSON. Five typed output handles: results (all), pdf, xml, text, image, plus a GIS handle for Shapefile/GeoJSON layers. Requires Chrome or Edge 86+. |
| FrameSenseSource | Local filesystem | Reads a folder pre-processed by the FrameSense CLI and emits one record per shot. Each record carries the representative frame as an imageDataUrl (base64 JPEG), enabling direct vision inference via KingsInference. Existing FrameSense analysis (shot scale classifications from scale_frames_sssabet, VLM answers from answer_frames_vlm) is surfaced as framesense.* fields. See FrameSense workflows below. |
| SampleDataSource | Local filesystem + fixtures | Loads pre-packaged collection samples from public/fixtures/ (e.g. Stonehenge antiquarian texts and modern archaeology papers, LLDS items, etc.). Useful for demonstrations, workshops, and testing without live API access. Emits five typed output handles: results (all), pdf, xml, text, image. |
| Node | Description |
|---|---|
| FieldDistribution | Faceted bar chart of value frequencies for any field. Click bars to toggle them as filters β matching records are emitted on the output handle and update live as upstream data changes. Array-valued fields (subject, country, creator) are expanded so each element is tallied individually. Bars sorted by count descending, capped at a configurable Top N. A status bar shows how many records match the current selection with a β clear button. |
| SmartFilter | Natural language β structured filter. Type a plain-English filter request (e.g. "Iron Age sites in England with coordinates", "objects made before 1700 with images") and click Translate & apply (or Ctrl/Cmd+Enter). The KCL inference API receives the actual field names and sample values discovered from your records, then returns a JSON conditions object which is evaluated deterministically β no eval(), no code generation. Each condition is displayed as a readable rule (title contains "Iron Age"). A match bar shows what proportion of records pass. Without a filter all records pass through unchanged; the β button clears the filter and restores pass-through. Supports: contains, equals, startsWith, endsWith, gt/lt/gte/lte, exists/notExists, in β all handle array-valued fields. Requires a KCL API key; defaults to arc:nano. |
| FilterTransform | Filters records by condition and/or mutates field values. See Filter / Transform below. |
| SpatialFilter | Draws a bounding box on an interactive Leaflet map; filters upstream records to those within the bbox. Note: MapOutput also includes an integrated spatial bbox filter β for workflows where you want to visualise and filter in a single step, use MapOutput instead. |
| Deduplicate | Removes duplicate records based on a chosen field value. First occurrence is kept; subsequent records sharing the same value for that field are discarded. Records missing the chosen field always pass through. The footer shows N in β M unique (K removed). Useful when aggregating results from multiple search nodes that may return overlapping result sets. |
| TimelineView | Resizable SVG horizontal timeline at year resolution. Handles ISO dates, bare years, and BCE dates. Filter mode β drag the date-range handles to restrict the visible window; records outside the range are suppressed on the output handle, making this a pass-through filter node as well as a visualisation. Toggle β€β₯ Fit to compress the full date range into the visible width. Pass-through output handle connects to any downstream node (TableOutput, Export, etc.). |
| Node | Description |
|---|---|
| KingsInference | Sends each upstream record to KCL's OpenAI-compatible inference API and enriches the record with the model's response. Requires an API key. Supports vision mode β when enabled, image data URLs in records (from LocalFolderSource, ImageView, or IIIF region capture) are sent as multipart image content. Auto-detects contentType: 'image' records; or specify a field to use a particular image field. Includes a prompt recipe bar β save and recall prompt strategies across workflows. See KCL Inference below. |
| KingsInferenceByField | Lighter-weight KCL inference on a single chosen field. Two modes: per-record (enriches each record individually with live token preview) and aggregate (collects all values into one prompt for a summary response). Template variables: {{value}}, {{field}}, {{count}}, {{values}}. Model-dependent content truncation (arc:nano 12k, arc:lite 32k, arc:nexus 64k chars). Includes prompt recipe bar. |
| QuickNote | Human annotation node β display-only, no runner, pass-through. Three modes selectable via the Note Β· Structured Β· Score tab bar: Note β free-prose textarea writes to _note (shared across the workflow via the notes store and surfaced in TableOutput); Structured β configure named {key, label} fields once, then fill them per record in a clean form; the node assembles the values into a JSON gold-standard and writes it to a target field (default _note); Score β constrained per-criterion pickers (configurable scale) write human_score (+ flat human_c* keys and optional prose reasons per criterion). The human counterpart to Evaluator. |
| Evaluator | LLM-as-judge node. Scores a candidateField against a referenceField on the same record using an ARC model (temperature 0). Emits per-criterion JSON scores β never a single aggregate. Built-in rubric presets: Extraction agreement, Interpretive agreement, Rubric-from-note. Template tokens {{__reference}} / {{__candidate}}; writes record.eval (scores, reasons, raw, status) plus flat eval_c* columns. Displays a judge-vs-human agreement readout when a human-score field is present. Tolerant JSON parsing β always recovers; marks failures as parse_error status rather than throwing. Requires a KCL API key. |
| URLContentFetch | Follows a URL field in each record, fetches the page (optionally via headless browser for JS-rendered pages), and adds fetchedContent (plain text) and fetchedHtml (cleaned body HTML). |
| HTMLExtract | Extracts a targeted section from fetchedHtml using a CSS selector. Connect HTMLPreview to visually browse the page and click-capture selectors. Toggle Preserve HTML structure to write raw HTML rather than stripped text β useful for passing markup to an inference model. |
| Reconciliation | Reconciles a chosen field against a Wikidata authority. See Reconciliation below. |
| WikidataEnrich | Fetches selected Wikidata properties for any reconciled QID field and appends them as wd_* fields. See Wikidata enrichment and linking below. |
| MergeByQID | Groups records from multiple upstream sources by shared Wikidata QID, producing one merged record per entity. See Wikidata enrichment and linking below. |
| XMLExtract | Evaluates an XPath expression against the content field of upstream records (typically XML or TEI documents). A schema inspector panel shows the element tree of the first record; click any element to build the XPath. Strips default XML namespaces before evaluation. Writes result to xmlContent. |
| Geocoding | Enriches a chosen place-name field using a two-tier gazetteer: Getty TGN (name search servlet β Linked Art JSON for coordinates) and Wikidata (wbsearchentities + P625 coordinates). Scores candidates using Dice string similarity + tier weight + cross-gazetteer corroboration. Auto-resolves when top score β₯ confidence threshold and gap to second candidate β₯ 20%; ambiguous results surface in an inline review panel for manual confirmation. Confirmed choices stored per node and persist in saved workflows. Candidate lists cached 30 days in localStorage; "clear cache" button forces a fresh network query. Adds decimalLatitude, decimalLongitude, and a geocoding.* namespace (geocoded status, source, authority URI, candidates, raw/cleaned place string, confidence). Connect output to MapOutput to plot geocoded records on a map. |
| SmartGeocoder | LLM-assisted place extraction and geocoding. Scans upstream records for place-name hints β either from all string fields or a user-selected subset β and calls the KCL inference API (arc:lite default) to identify the most likely canonical place name. The extracted place name is then resolved through the same Getty TGN β Wikidata two-tier gazetteer used by the regular Geocoding node, writing decimalLatitude, decimalLongitude, and a smartGeo.* namespace. Designed for records where the place name is embedded in prose (e.g. a manuscript description or an archival summary) rather than held in a dedicated field. Requires a KCL API key. |
| Node | Description |
|---|---|
| Citation | Paginated bibliography drawn from _citation metadata stamped by source runners. Supports Copy all and Download .txt for a formatted reference list. |
| Export | Downloads upstream records as CSV, JSON, or GeoJSON. *_reconciled objects are expanded to _qid/_label/_confidence/_status columns in CSV. |
| JSONOutput | Syntax-highlighted JSON viewer. Double-click to expand to full-screen. |
| KingsInferenceOutput | Card-based display of KCL inference responses. Each record gets an expandable card showing the response, model used, and processing timestamp. Copy button per card. |
| MapOutput | Leaflet map. Plots records with decimalLatitude/decimalLongitude. Click a marker for a popup with title, date, and source link. Integrated spatial filter: click Draw bbox, drag a bounding box on the map, then Run βΆ β only records within the box are emitted; records outside are dimmed. A coordinate summary shows N/S/E/W bounds. A green results output handle pipes the filtered (or all) records to downstream nodes. Also accepts GIS vector layers via the GIS handle from LocalFolderSource. |
| TableOutput | Paginated table. Merges records from multiple upstream nodes. Pass-through output handle for chaining to Map, Export, etc. Double-click to expand to full-screen. Toolbar: show all columns + expand namespaces (flattens service namespace objects into dot-notation columns). Page size selector (10 / 25 / 50 / 100 rows). Column sort: click any column header to sort ascending, click again for descending, third click clears the sort. Text filter: search box above the table filters across all fields (including namespace sub-objects) live as you type. |
| ComparisonReport | Read-only evaluation report β no runner, skipped by Run All. Map five report roles (original, note / reference, response / candidate, judge score, human score) to upstream fields via dropdowns. Renders per-record side-by-side cards and an aggregate agreement matrix (counts, means, and percentage-agreement only β no inferential statistics). Divergence counts double as live card filters; click a count to show only divergent records. Double-click the node to open a full-screen projector view for presentations. Agreement is computed over scored records only. |
| SaveSearch | Serialises upstream records with a metadata envelope to a .nfcs.json file. Shows record count, per-source breakdown, and auto-suggested filename. Native Save As⦠dialog on Chrome/Edge; auto-download on Firefox. |
ParamNode ββ
βΌ
ARIADNESearch ββββββββββββββββββββββββββββββββββββββββββββββββββ
HSDSSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
BodleianSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
EuropeanaSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
GBIFSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
LLDSSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
MDSSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
LocalFileSource ββββββββββββββββββββββββββββββββββββββββββββββββββ€
LoadSavedSearch ββββββββββββββββββββββββββββββββββββββββββββββββββ€
βΌ β
FilterTransformNode ββββββββββββββββββββββββββββββββββββ β€
SpatialFilterNode ββββββββββββββββββββββββββββββββββββ β€
FieldDistributionNode βββββββββββββββββββββββββββββββββββ β€ β click bars to facet-filter
TimelineView βββββββββββββββββββββββββββββββββββ β€ β drag range handles to filter
β
LocalFolderSource βββΊ KingsInference ββββββββββββββββββββββββββββββββ β€
ImageView (IIIF) βββΊ KingsInferenceByField βββββββββββββββββββββββββββ β€
URLContentFetch βββΊ HTMLExtract ββββββββββββββββ β€
XMLExtract ββββββββββββββββββββββββββββββββ β€
Reconciliation βββΊ WikidataEnrich βββΊ MergeByQID β€
βΌ
TableOutput βββΊ Export
Citation
JSONOutput
MapOutput βββ LocalFolderSource (GIS)
KingsInferenceOutput
SaveSearch βββ (any data-handle source)
All data nodes expose a data input handle (left) and a results output handle (right) unless otherwise noted. You can chain them in any order and branch to multiple output nodes simultaneously.
The useUpstreamRecords hook merges records from all edges connected to a node's input handle, so a single Table or Map node can aggregate several source nodes at once.
- βΆ Run (on individual nodes) β execute that node only.
- βΆβΆ Run All (top bar) β discovers every runnable node, builds a topological order using Kahn's algorithm, and executes nodes wave-by-wave: all source nodes in parallel first, then each processing layer in dependency order. If one node errors, downstream dependants are skipped but unrelated branches continue.
All node types are included in Run All except LocalFolderSource and LocalFileSource (file/folder selection requires a user gesture and cannot be automated). Run those nodes manually before clicking Run All.
Nodes can be bundled into a labelled, collapsible group for visual organisation. Groups are purely cosmetic β they do not affect data flow.
- Select two or more nodes on the canvas (click, Shift+click, or drag a selection box).
- Click π¦ Group (N) in the top bar β the selected nodes are wrapped in a blue dashed container with a default label.
- Double-click the label to rename it.
- Select a group node and click π€ Ungroup to dissolve it β all children are returned to the canvas at their absolute positions.
Each group has a small toolbar (βΌ / βΆ) at the top-right corner. Click βΌ to collapse the group into a compact pill; click βΆ to expand it again. Collapsed groups still participate in data flow β edges are re-routed through the group pill.
A group automatically expands its bounding box when a child node is resized or when a new node is dragged inside it (expandParent). Groups are also restored to their expanded state when a workflow is reloaded.
The β§ toolbar button duplicates a group together with all its children and any internal edges. External edges (connecting group children to nodes outside the group) are not duplicated.
Every adapter maps its raw API response to UnifiedRecord before writing to the canvas. Output nodes consume only UnifiedRecord[].
id β globally unique, service-prefixed: "gbif:12345", "ariadne:<hash>", "bodleian:<id>"
_source β service identifier: "gbif" | "llds" | "ariadne" | "mds" | "europeana" | "bodleian"
_sourceId β native record ID within the service
_sourceUrl β link back to the record in the service's own UI
_pid β persistent identifier (DOI, Handle, ARK) when available
_citation β citation metadata stamped by source runners
title β best available display title
description β abstract or description
creator β author(s) β string or string[]
date β publication or event date
subject β subject keywords β string or string[]
language β language code
decimalLatitude, decimalLongitude β used by MapOutput
gbif.* β full raw GBIF occurrence object
llds.* β LLDS handle, branding, itemType
ariadne.* β ARIADNE temporal, country, spatial, contributor, subjects, identifier
hsds.* β HSDS landingPage, contributor, temporal, spatial, ariadneSubject, nativeSubject, derivedSubject, dataType, accessRights
mds.* β MDS field map (condition, materials, dimensions, provenance, β¦)
europeana.* β provider, dataProvider, rights, thumbnail, shownAt, completeness
bodleian.* β shelfmark, objectType, dateRange, manifest (IIIF manifest URL)
fetchedUrl, fetchedContent, fetchedHtml, fetchStatus, fetchedAt β added by URLContentFetch
htmlSelector β added by HTMLExtract
xmlContent, xmlXPath β added by XMLExtract
kclModel, kclPrompt, kclResponse, kclProcessedAt β added by KingsInference nodes
ollamaModel, ollamaPrompt, ollamaResponse, ollamaProcessedAt β added by Ollama nodes (hidden)
content, contentType, mimeType β FileRecord fields (from LocalFolderSource / ImageView)
After reconciliation, records also carry ${fieldName}_reconciled keys (see Reconciliation).
KingsInference and KingsInferenceByField connect to KCL's hosted OpenAI-compatible inference API (https://api.ai.create.kcl.ac.uk/v1) via the /kcl-proxy route. An API key is required.
- Drag a KingsInference node onto the canvas.
- Enter your API key in the API Key field. The node fetches available models from the API and populates the model dropdown.
- Configure the system prompt, user prompt template, temperature, and max tokens.
Use {{fieldName}} placeholders in the user prompt template. Click βΌ fields to see the available substitution tokens drawn from the first upstream record. Common tokens:
| Token | Value |
|---|---|
{{content}} |
Primary text content of the record (content, fetchedContent, description, or title in order of availability) |
{{title}} |
Record title |
{{description}} |
Record description / abstract |
{{anyField}} |
Any field from the record by name |
{{_lineage}} |
The context accrual token β a plain-English narrative of the pipeline that produced the records. See Context accrual below. |
Tick Vision to enable multipart image content. When active, the node automatically detects image data URLs in upstream records and sends them alongside the text prompt to any vision-capable model.
- Auto-detect: first checks for
contentType === 'image'(records from LocalFolderSource, ImageView), then scans all string fields fordata:image/prefixes. - Field override: select a specific field from the dropdown when multiple image fields are present.
- In vision mode,
{{content}}resolves todescriptionortitlerather than the raw data URL.
Works identically but processes a single chosen field per record. Per-record mode enriches each record; aggregate mode collects all field values into one prompt for a summary response.
Enriched records gain kclResponse, kclModel, kclPrompt, and kclProcessedAt fields. Connect a KingsInferenceOutput node to display responses as expandable cards, or pass to TableOutput or Export.
Both KingsInference and KingsInferenceByField include a prompt recipe bar for saving and recalling prompt strategies. Built-in recipes are included (marked with β ):
Standard node recipes (KingsInference):
- Extract persons β JSON
- Extract places β JSON
- Summarise briefly
Field node recipes (KingsInferenceByField):
- Per-record: Extract persons from field β JSON, Extract places from field β JSON, Summarise record field
- Aggregate: Thematic summary, List unique entities
To use: select a recipe from the dropdown, click Apply to populate the system prompt and template. Click Saveβ¦ to create custom recipes β they persist in localStorage and are available across sessions.
Put {{_lineage}} anywhere in a KingsInference, Ollama, or Evaluator prompt and, at run time, it is
replaced with a plain-English narrative of the upstream pipeline history β which services were searched with
what terms, how records were filtered, transformed, reconciled, geocoded, or merged, and how many records flowed
through each stage. The model sees not just the records but how they came to be, which materially improves
summaries, comparisons, and judgements.
Example substitution for a two-branch workflow:
Branch A:
1. Searched ARIADNE (pan-European archaeology portal) for "hillfort" with filters: Country=United Kingdom; retrieved 120 results.
Branch B:
1. Queried Wikidata (SPARQL) β instances of Q744099; retrieved 95 results.
Then:
1. Merged records from multiple sources by shared Wikidata QID: 61 merged entities, 14 unmatched (dropped).
2. Filtered records where title contains "Iron Age": 23 of 61 records passed.
How it works (design in docs/context-accrual.md):
- The lineage is derived on demand from the node graph β
collectLineage(nodeId, nodes, edges)walks the upstream subgraph over data-flow edges (param wires are configuration, not data) and returns a topologically ordered history;lineageToNarrative(graph)renders it LLM-ready. Nothing is stamped on records. - Record counts are read from the results store with pass-through inheritance, so display-only nodes (TableOutput, QuickNote, FieldDistribution) sitting in the chain report the records flowing through them rather than breaking the story.
- Linear chains render as a numbered list; parallel branches as lettered sections with shared ancestors described once; joins (e.g. MergeByQID) and everything after them under Then:. A ~2,000-character budget drops the earliest steps whole β never mid-sentence.
- If an upstream node's settings changed since it last ran, the narrative is prefixed with an explicit staleness warning.
- Templates without the token are untouched β zero cost unless you opt in.
FrameSense is a Python CLI that pre-processes video collections into a structured folder hierarchy of shots and frames. This app reads a pre-processed collection β it does not process video itself.
Run these FrameSense operators on your video collection:
python framesense.py make_shots_scenedetect # auto-detect shot boundaries (PySceneDetect)
python framesense.py make_frames_ffmpeg # extract first, middle, last frame per shot
python framesense.py scale_frames_sssabet # optional: classify shot scale (ECU/CU/MS/FS/LS)The resulting folder structure is:
<collection>/
<video>/
[<clip>/]
shots/
001/
middle.jpg β representative frame
frames.json β existing analysis (shotScale, VLM answers if run)
- Drag FrameSenseSource onto the canvas.
- Choose a Frame filter β Middle (recommended, one image per shot) keeps memory usage low.
- Click π¬ Pick Folder and select the pre-processed collection root.
- The node scans recursively and emits one
UnifiedRecordper shot, containing:framesense.collection,framesense.video,framesense.clip,framesense.shotframesense.frameFileβ the file loaded (e.g.middle.jpg)framesense.shotScaleβ ifscale_frames_sssabetwas run (e.g.MS,CU)- Any existing VLM answers already in
frames.jsonasframesense.<questionKey> imageDataUrlβ base64 JPEG of the frame
Run All skips FrameSenseSource β it requires a user gesture. Pick the folder and click Re-scan manually before running downstream nodes.
Connect FrameSenseSource results β KingsInference data. KingsInference auto-detects imageDataUrl and shows the Vision checkbox β tick it. The image is attached as a multipart message alongside your prompt; {{imageDataUrl}} does not need to appear in the template text.
Recommended prompt for a quick functional test:
System: You are a film analyst. Be concise.
Prompt:
Describe this shot in one sentence. Note the apparent shot scale (extreme close-up,
close-up, medium, full, or long shot) and what is depicted.
This is immediately verifiable: if scale_frames_sssabet pre-processing was run, compare the model's shot scale description against framesense.shotScale in TableOutput.
For structured output suitable for downstream processing, use a JSON prompt:
Prompt:
Analyse this frame. Respond in JSON with these keys:
"description" (one sentence), "shot_scale" (one of: ECU, CU, MS, FS, LS),
"setting" (interior/exterior/unclear), "people_visible" (true/false).
The kclResponse field then contains parseable JSON you can filter on or aggregate.
After per-shot inference, wire a KingsInferenceByField node downstream in aggregate mode to summarise across a whole video:
- Field:
kclResponse - Mode: Aggregate
System: You are a film analyst helping to catalogue archival video footage for humanities research.
Prompt:
The following are shot-level analyses from a video, one per line. Each contains
a description, shot scale, setting, and whether people are visible.
{{values}}
Write a short catalogue summary (3-5 sentences) covering: the overall subject and
setting of the footage, the range of shot scales used, and whether people feature
prominently. Then list 3-5 keywords suitable for archival indexing.
Tip β context window management: if the collection is large, the concatenated shot analyses may approach the model's context limit. Use a FilterTransform node upstream of KingsInferenceByField to narrow records to a single
framesense.videoorframesense.collectionbefore aggregating.
[FrameSenseSource]
β results
[FilterTransform] β optional: filter by framesense.shotScale or framesense.collection
β results
[KingsInference] β Vision on, per-shot question (JSON prompt recommended)
β results
[KingsInferenceByField] β aggregate mode, field: kclResponse, video summary
β results
[TableOutput]
[Export] β CSV/JSON for archival deposit
Three nodes work together to support rigorous human and model-based evaluation of AI-generated outputs:
[Source node]
β data
[KingsInference] β generate candidate responses (kclResponse)
β results
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βΌ βΌ
[QuickNote β Note / Structured mode] [QuickNote β Score mode]
Annotate records with a gold-standard Rate each record's candidate against
reference (free prose or structured human-defined criteria
JSON into _note or a custom field) β human_score + human_c* keys
β β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββ
βΌ
[Evaluator]
LLM-as-judge: scores candidateField
vs referenceField, per-criterion
β eval.scores, eval_c* keys
+ judge-vs-human agreement readout
β
[ComparisonReport]
Side-by-side cards + aggregate matrix
Judge score / human score comparison
| Role | Node | Field written |
|---|---|---|
| Candidate | KingsInference / KingsInferenceByField | kclResponse (or any field) |
| Reference / gold standard | QuickNote (Note or Structured mode) | _note or a custom field |
| Human score | QuickNote (Score mode) | human_score, human_c* |
| Judge score | Evaluator | eval.scores, eval_c* |
| Report | ComparisonReport | β read-only display |
Criteria are matched by convention, not code. The criterion keys in QuickNote (Score mode) must use the same names as the criteria in the Evaluator rubric for the agreement maths in ComparisonReport to be meaningful. A mismatch produces empty agreement cells, not an error.
QuickNote nodes are annotation-only and do not appear in the βΆβΆ Run All execution graph. Fill them manually (or use Structured mode to speed up gold-standard entry). Evaluator runs automatically as part of Run All. ComparisonReport is also annotation-only and skipped by Run All β open it to review results after Evaluator has run.
ImageView is a resizable node in the Inspection group. It has a source output handle (green, top right) so the currently displayed image can be piped to inference nodes.
- Field picker β select any field from upstream records that contains an image data URL or remote HTTP image URL.
- URL row β paste any public image URL directly; this overrides the field picker when set.
- The output handle emits a single record:
{ content: dataUrl, contentType: 'image', mimeType, title, ...sourceRecord }.
Accepts IIIF Presentation API v2 or v3 manifest URLs. Upstream records from BodleianSearch (and other services that include IIIF manifests) populate a From upstream picker automatically β no copy-pasting required.
Zoom requests are served at tiered resolutions via the IIIF Image API (!600,600 β !1200,1200 β !2400,2400 β max) to avoid fetching full-resolution masters unnecessarily.
Once a manifest is loaded, the region annotator toolbar appears above the canvas navigation:
- Click + Region to enter draw mode (button turns orange).
- Drag a rectangle over any area of the displayed image. A blue semi-transparent box is committed and labelled automatically (Region 1, Region 2, β¦).
- Delete any region by clicking the red Γ in its top-right corner.
- Click Capture (or Capture N when regions exist) to fetch each region as a base64-encoded image and write the records to the output handle.
How capture works:
- For IIIF Image API services: each region uses the IIIF
pct:x,y,w,hparameter to request only the cropped area at up to 1024 Γ 1024 px ({serviceUrl}/pct:.../!1024,1024/0/default.jpg). - For direct-URL canvases (no IIIF Image API): the full canvas image is fetched.
- Direct
fetchis tried first; falls back to/url-proxyfor CORS-restricted servers. - Each output record:
{ content: dataUrl, contentType: 'image', mimeType: 'image/jpeg', title, regionIndex, regionBounds, canvasLabel, manifestTitle, ... }. - If no regions are drawn, Capture fetches the whole canvas as a single record.
Connect the output handle to KingsInference (with Vision enabled) to send annotated IIIF image regions to the inference model. Regions and their labels are saved in the workflow file.
Toggle βΉ Info to reveal: IIIF manifest metadata (title, date, attribution, provider, rights, pixel dimensions from info.json, tile size, compliance profile) or local image metadata (pixel dimensions, estimated file size, and EXIF β make/model, date taken, exposure, aperture, ISO, GPS coordinates β parsed inline from the first 64 KB of JPEG files without external libraries).
FilterTransform operates in three modes (selectable via tabs):
Add one or more filter rows. Each row specifies:
- Field β any field, including dot-notation namespace fields (e.g.
gbif.stateProvince,ariadne.contributor) - Operator β
contains,=,starts with,>,<,is empty,not empty - Value β text or number
Multiple rows are combined with an AND / OR toggle.
| Operation | What it does |
|---|---|
| Rename field | Copies a field to a new key. Optional drop checkbox removes the original. |
| Lowercase / Uppercase | In-place case conversion. Array values converted element-by-element. |
| Truncate | Trims a field to a maximum character length and appends β¦. |
| Extract | Slices a substring by start/end index, or captures a regex match. Writes to a new field. |
| Concatenate | Merges two fields into a new key with a configurable separator. |
Filter runs first, then transforms are applied to the reduced set.
ReconciliationNode enriches a chosen field by matching its unique values against a Wikidata authority via the W3C Reconciliation API.
- Connect an upstream node to the
datahandle and select the field to reconcile. - Select the authority:
| Field | Authorities |
|---|---|
creator |
Wikidata People (Q5) |
country, spatialCoverage |
Wikidata Places (Q618123) |
scientificName, species, genus |
Wikidata Taxa (Q16521) |
institutionCode |
Wikidata Organisations (Q43229) |
| (any other field) | Wikidata Items |
- Set the confidence threshold (0.5β1.0, default 0.8) and click βΆ Reconcile.
Each augmented record gains a ${fieldName}_reconciled key. In TableOutput, reconciled cells render as coloured pills β green (resolved, confidence β₯ threshold) or amber (flagged for review). QIDs are clickable links to wikidata.org.
Fetches structured properties from Wikidata for any reconciled field and appends them as wd_* fields (e.g. wd_IUCNStatus, wd_country). Works directly against the Wikidata API β no proxy required.
Tick the properties you want grouped by domain (General, Taxon, Place, Person, Heritage) or enter custom P-IDs. wikibase-item values are resolved to English labels in a second batch call.
Recommended order:
Reconciliation β MergeByQID β WikidataEnrichβ merge first so enrichment is applied once per entity rather than once per record.
Groups records from any number of upstream sources by shared Wikidata QID and emits one merged record per entity. Each merged record contains _qid, _sourceCount, _sources, and all source fields prefixed by service name (e.g. gbif_scientificName, ariadne_title). Toggle Keep unmatched records to pass through records with no reconciled QID unchanged.
(Experimental group β enable Advanced mode to see it.) Query knowledge graphs over SPARQL without needing to know SPARQL β or any entity identifiers. The node solves the two things that normally make SPARQL unusable for non-specialists: finding the right IDs and writing the query.
Every place the query needs an identifier is a live, as-you-type search backed by wbsearchentities β the same
API behind Wikidata's own autocomplete (browser-direct, no key needed):
- find (instance-of): type
hillfortβ pick hillfort (Q744099) from a dropdown showing label, Q-id, and disambiguating description. Curated suggestions appear instantly when the box is empty. - Property filters: the property box searches Wikidata's properties β type
time periodβ P2348,artistβ creator (P170). The curated property groups appear as instant seeds. - Filter values: type a name (
J. M. W. Turnerβ Q159758) or leave free text for a CONTAINS match. - Any Q-id/P-id already in the node shows a resolved-label chip (e.g.
Q3305213 β painting) linking to Wikidata β so you always know what an opaque identifier actually is. A red chip flags a nonexistent entity.
Every builder change regenerates the query into a read-only preview; β Edit as raw SPARQL is the escape hatch (hand edits are kept until a builder control changes).
Type a research question in plain English (e.g. "paintings by either J. M. W. Turner or John Constable"), pick
an ARC model (arc:nano/lite/nexus/apex), and click β¨. The generated SPARQL lands in Raw mode for review β
but first every wd:Q⦠it references is verified against live Wikidata:
- The model must declare what each QID it used is meant to be; the app checks the QID exists and that it appears in the live search results for that name (alias-aware β "William Turner" correctly verifies Q159758, label "J. M. W. Turner").
- Hallucinated or mismatched QIDs are automatically replaced with the top live-search hit, and a per-entity
report appears under the query:
β Q159758 J. M. W. Turner(verified) /β Q11436 "aircraft" β Q159758 J. M. W. Turner (from "J.M.W. Turner") βΎ(auto-repaired β click for alternates, including one-click restore of the original) /β(no match found). This matters: LLMs routinely emit plausible-looking but wrong QIDs, which produce syntactically valid queries that silently return zero rows. - Verification is tolerant β if Wikidata is unreachable the query is kept unchanged, never blocked.
| Endpoint | Status | Features |
|---|---|---|
| Wikidata (default) | Live | Builder, keyword seed, β¨ NL assist + grounding, entity/property lookup |
| Getty Vocabularies (AAT/TGN/ULAN) | Live | Raw SPARQL only (gvp/xl/luc/skos prefixes built in); switching inserts a working AAT sample query |
| British Library BNB | Offline | Greyed out β the BNB linked-data platform has been down since the BL cyber-incident; the entry and proxy route are ready to enable the day BL restores it |
Old saved workflows carry no endpoint field and load against Wikidata unchanged. Non-Wikidata endpoints force Raw mode (the builder and assist are Wikidata-specific and would mislead).
- Wirable query (keyword) and limit handles at the standard search-node positions; a query without an
explicit
LIMITgets the limit row appended (an in-query LIMIT wins). - Result bindings land under the
sparql.*namespace;?item/?itemLabel/?itemDescriptionmap to id/title/description, WKTPoint(lon lat)values map todecimalLatitude/decimalLongitude(β MapOutput), and the trailing Q-id of?itemis written to_qidβ so results plug straight into WikidataEnrich and MergeByQID alongside reconciled records from other services. - Fixture mode (π¦) works as on every search node; error responses are condensed to a readable message
(e.g.
Wikidata 400: MalformedQueryException) instead of a Java stack trace.
"Paintings by either Turner or Constable" β builder filters AND together, so "either" needs one raw edit:
- Keep the default find: painting; add filter creator (P170) β type
J. M. W. Turner, pick Q159758. - Click β Edit as raw SPARQL and change
?item wdt:P170 wd:Q159758 .to?item wdt:P170 ?creator . VALUES ?creator { wd:Q159758 wd:Q159297 }. - Set the limit and βΆ Run (~1,100 paintings match).
Or just ask the β¨ assist for it β or run two SPARQL nodes (one per artist) into a shared TableOutput, the canvas-native way.
Data-reality tip: Wikidata's tagging is uneven β e.g. only ~27 of 4,150 UK hillforts carry a time period statement, so a hard
P2348 = Bronze Agetriple discards nearly everything. For period-faceted archaeology, combine sources: pull coordinates from SPARQL, facet periods via ARIADNESearch, and merge β that federation is what this workbench is for.
Follows a URL field in each upstream record and fetches the page, adding fetchedContent (plain text) and fetchedHtml (cleaned body HTML). URL field is auto-detected from fields whose name or value suggests a URL; also scans service namespace objects for dot-notation paths like ariadne.identifier.
Options: Wait for JS rendering (Puppeteer), Wait for strategy, Max chars, Timeout.
Extracts a targeted section from fetchedHtml using a CSS selector, writing the result into fetchedContent. Connect HTMLPreview β HTMLExtract for a fully visual selector workflow: click any element in the preview pane to capture its selector. Toggle Preserve HTML structure for markup-aware extraction.
Export downloads upstream records. Select format:
| Format | Description |
|---|---|
| CSV | Flat table. *_reconciled objects expanded to _qid, _label, _confidence, _status columns. Namespace objects excluded. |
| JSON | Full record graph as a pretty-printed JSON array. |
| GeoJSON | FeatureCollection of records with decimalLatitude and decimalLongitude. |
Files named nfcs-export-YYYY-MM-DD.{ext}.
SaveSearch and LoadSavedSearch cache search results for reproducible demos and offline use.
Saved files use a .nfcs.json envelope wrapping the record array with _nfcs metadata: savedAt, sources, sourceCounts, recordCount, searchParams (the full configuration of every upstream source node, keyed by type and ID).
Any JSON file produced by Export (format: JSON) can also be loaded into LoadSavedSearch β it will display as "N records β no metadata (raw export)".
When running the Docker deployment (deploy/express-server branch), every time a participant clicks πΎ Save a copy of their workflow is silently posted to the server and written to /app/data/workflows/ inside the container, in addition to the usual local browser download. Files are stored in a named Docker volume (workflow_saves) so they survive container restarts.
Each file is named by UTC timestamp plus a random suffix to prevent collisions when multiple participants save simultaneously:
2026-05-28T14-30-00-000Z-x3k9mf.json
Files contain the full workflow JSON (nodes, edges, configuration) plus two server-injected fields:
| Field | Description |
|---|---|
serverReceivedAt |
ISO timestamp of when the server received the save |
remoteIp |
Participant's IP address β useful for grouping saves by participant |
docker compose exec app ls /app/data/workflows/Using Docker Compose v2 (docker compose cp):
docker compose cp app:/app/data/workflows/ ./workshop-saves/This creates a workshop-saves/ directory in your current folder containing all .json files.
If you need to use plain docker cp instead, first get the container name:
docker ps --format '{{.Names}}'
# e.g. nfcs-poc-app-1
docker cp nfcs-poc-app-1:/app/data/workflows/ ./workshop-saves/Each file is a standard JSON object you can open in any text editor or analyse with tools like jq:
# Count saves per participant IP
jq -r '.remoteIp' workshop-saves/*.json | sort | uniq -c | sort -rn
# List all node types used across all saved workflows
jq -r '[.[].nodes[].type] | unique[]' workshop-saves/*.json | sort | uniq -c | sort -rnEvery active search node ships with a fixture mode for offline and workshop use.
| Control | Behaviour |
|---|---|
| π¦ toggle | When checked, βΆ Load fixture loads pre-baked results from public/fixtures/ instead of calling the live API. |
| πΎ button | Downloads the current results as a {nodeType}-{query}.json fixture file. Drop into public/fixtures/ and commit to make it available to all. |
The fixture filename derives from the search query (inline or wired from a ParamNode), e.g. bodleianSearch-manuscript.json. Mismatches produce a descriptive 404 error naming the expected file.
| Query | ARIADNE | GBIF | LLDS | MDS | Europeana |
|---|---|---|---|---|---|
stonehenge |
β | β | β | β | β |
wordsworth |
β | β | β | β | β |
roman coin |
β | β | β | β | β |
public/fixtures/LLDS Collections/ contains actual collection items from the Oxford Text Archive β TEI-XML transcriptions, Dublin Core and METS metadata, EPUBs, and plain-text versions for Stonehenge and Wordsworth topics. Use with LocalFolderSource at workshops.
| Prefix | Target | Reason |
|---|---|---|
/llds-proxy/β¦ |
https://llds.ling-phil.ox.ac.uk/llds/β¦ |
No CORS |
/mds-proxy/β¦ |
https://museumdata.uk/β¦ |
No CORS |
/reconcile-proxy/β¦ |
https://wikidata.reconci.link/β¦ |
307 redirect strips CORS headers |
/kcl-proxy/β¦ |
https://api.ai.create.kcl.ac.uk/β¦ |
KCL inference API β avoids CORS for hosted inference |
/ollama/β¦ |
http://localhost:11434/β¦ |
Cross-port CORS for local Ollama |
/url-proxy?url=β¦ |
any URL | Vite middleware; sidesteps CORS for arbitrary URL fetching |
/hsds-proxy/β¦ |
https://hsds.ac.uk/β¦ |
No CORS |
Production note: This proxy is development-only. The
deploy/express-serverbranch includes an Express server that replicates all proxy routes for deployed instances.
The top bar contains a π¬ Assistant button that toggles a right-hand chat panel backed by KCL inference.
- Model:
arc:apexvia/kcl-proxy/v1/chat/completions(streaming, server-sent events). - Rendering: Responses are rendered as GitHub-Flavoured Markdown β tables, code blocks, and lists display correctly.
- Knowledge: The assistant carries a full built-in node reference (all node types, handles, field names, prompt tokens, and architectural notes) so it can answer questions about the interface without needing any configuration.
- "What node should I use to extract text from a PDF?"
- "How do I reconcile a field against Wikidata?"
- "Suggest a workflow for comparing AI responses against human annotations."
- "What fields does ARIADNESearch add to a record?"
A default KCL API key may be pre-configured. If the panel shows an API error, enter a valid key in any KingsInference node β the same key is used by the assistant panel. You can also override it directly in the src/components/ChatSidebar.tsx API_KEY constant.
- Drag ARIADNESearch, BodleianSearch, and EuropeanaSearch onto the canvas.
- Type a query (e.g.
Stonehenge) into the inline query fields on all three nodes. - Connect all three outputs to a single TableOutput node.
- Click βΆβΆ Run All β results from all three services appear merged in one table.
- Run a BodleianSearch query (e.g.
illuminated manuscript). - Connect the output to an ImageView node and switch it to IIIF mode.
- Use the From upstream picker to select a manuscript β the manifest loads automatically.
- Navigate to a page of interest using the βΉ / βΊ canvas buttons.
- Click + Region and drag boxes around specific areas (a decorated initial, a marginal annotation, a seal).
- Click Capture 3 β each region is fetched as a base64 JPEG via the IIIF Image API.
- Connect the ImageView output handle to a KingsInference node with Vision enabled.
- Write a prompt such as "Describe what you see in this region of a medieval manuscript."
- Click βΆ Run on KingsInference β each region is analysed individually.
- Connect the output to KingsInferenceOutput to browse responses as expandable cards.
- Drag LocalFolderSource β KingsInference β KingsInferenceOutput and connect them.
- Click π Pick Folder and select a folder of JPEG or PNG images.
- On KingsInference, enter your API key, select a vision-capable model, tick Vision, and write a prompt using
{{content}}(or leave it blank to describe the image). - Click βΆ Run on LocalFolderSource, then βΆβΆ Run All.
- Run ARIADNESearch and GBIFSearch with overlapping subjects.
- Connect both to a Reconciliation node (field
titleorscientificName, authority: Wikidata Items / Taxa). - Connect Reconciliation to MergeByQID β records that resolved to the same Wikidata entity are merged.
- Connect to WikidataEnrich and tick properties of interest.
- Connect to TableOutput and enable expand namespaces to see cross-service fields side-by-side.
- On each search node, type a query matching a bundled fixture (e.g.
stonehenge) and tick π¦. - Click βΆ Load fixture β results load instantly from the repo with no network calls.
- For local text analysis, drag a LocalFolderSource onto the canvas and point it at
public/fixtures/LLDS Collections/stonehenge. - Connect the XML handle to an XMLExtract node or the main output to KingsInference for AI-assisted analysis.
- Run any source node with
_sourceUrlfields. - Add URLContentFetch, select the URL field, and fetch pages.
- Add HTMLPreview β click any element in the rendered page to capture its CSS selector.
- Add HTMLExtract, paste the captured selector, and extract the section.
- Add KingsInferenceByField in per-record mode with
fetchedContentand a targeted extraction prompt.
nfcs-poc/
βββ CLAUDE.md # Architecture notes and API references (dev only)
βββ vite.config.ts # Dev server + CORS proxy rules + /url-proxy middleware
βββ public/
β βββ fixtures/ # Pre-baked search results + sample collection material
β βββ README.md
β βββ *.json # {nodeType}-{query}.json
β βββ LLDS Collections/
βββ src/
βββ App.tsx # Canvas, sidebar, Run All, save/load, node factories
βββ types/
β βββ UnifiedRecord.ts
β βββ savedSearch.ts
βββ store/
β βββ resultsStore.ts # Out-of-band record store (avoids React state bloat)
βββ hooks/
β βββ useUpstreamRecords.ts
βββ components/
β βββ ConnectionSuggestions.tsx # Context-aware node suggestion popup
β βββ ChatSidebar.tsx # KCL Assistant chat panel
βββ nodes/
β βββ index.ts
β βββ ParamNode.tsx
β βββ CommentNode.tsx
β βββ ARIADNESearchNode.tsx
β βββ BodleianSearchNode.tsx # Bodleian Digital Collections
β βββ EuropeanaSearchNode.tsx
β βββ GBIFSearchNode.tsx
β βββ LLDSSearchNode.tsx
β βββ MDSSearchNode.tsx
β βββ GroupNode.tsx # Group container node
β βββ EvaluatorNode.tsx # LLM-as-judge evaluation
β βββ QuickNoteNode.tsx # Human annotation and scoring
β βββ ComparisonReportNode.tsx # Comparison report output
β βββ LocalFileSourceNode.tsx
β βββ LocalFolderSourceNode.tsx
β βββ SaveSearchNode.tsx
β βββ LoadSavedSearchNode.tsx
β βββ FilterTransformNode.tsx
β βββ SpatialFilterNode.tsx
β βββ FieldDistributionNode.tsx
β βββ TimelineOutputNode.tsx # TimelineView β filter + timeline visualisation
β βββ KCLNode.tsx # KingsInference β per-record, vision-capable
β βββ KCLFieldNode.tsx # KingsInferenceByField β single-field inference
β βββ KCLOutputNode.tsx # KingsInferenceOutput β card display
β βββ ReconciliationNode.tsx
β βββ WikidataEnrichNode.tsx
β βββ MergeByQIDNode.tsx
β βββ OllamaNode.tsx # (hidden β local Ollama)
β βββ OllamaFieldNode.tsx # (hidden β local Ollama)
β βββ OllamaOutputNode.tsx # (hidden β local Ollama)
β βββ URLFetchNode.tsx
β βββ HTMLSectionNode.tsx
β βββ HTMLPreviewNode.tsx # WYSIWYG CSS selector capture
β βββ XMLSectionNode.tsx
β βββ QuickViewNode.tsx
β βββ ImageViewNode.tsx # Images + IIIF + region annotator + output handle
β βββ TableOutputNode.tsx
β βββ JSONOutputNode.tsx
β βββ MapOutputNode.tsx
β βββ CitationNode.tsx
β βββ ExportNode.tsx
β βββ SmartGeocoderNode.tsx # LLM-assisted place extraction + geocoding
β βββ SmartFilterNode.tsx # Natural-language β structured filter
β βββ SourceProfileNode.tsx # Schema inspector + AI narrative
β βββ QuickStartNode.tsx # AI workflow planner
β βββ ReconciledCell.tsx
β βββ ExpandedOutputPanel.tsx
βββ utils/
βββ nodeRunners.ts # Registry: node type β NodeRunner
βββ runWorkflow.ts # Topological executor (Kahn's algorithm)
βββ workflowIO.ts
βββ nodeIdCounter.ts
βββ upstreamRecords.ts
βββ fixtureUtils.ts
βββ reconciliationService.ts
βββ filterTransformUtils.ts
βββ exportUtils.ts
βββ fileReaders.ts
βββ citationUtils.ts
βββ run<Name>Node.ts # One runner per runnable node type
βββ *Adapter.ts # Service-specific response β UnifiedRecord
Two developer documents cover extension:
- docs/federation-baseline.md β the checklist that makes an external data service a good federation candidate (open API, CORS, PIDs, licence metadata, pagination, field completeness), with GBIF and Europeana scored as worked reference implementations.
- docs/context-accrual.md β the design for workflow context accrual: how pipeline history (search terms, services combined, filters, transforms) is derived from the node graph and surfaced to downstream LLM nodes via the
{{_lineage}}prompt token (implemented β see Context accrual).
The step-by-step registration checklist for a new node lives in CLAUDE.md. Adapter output is contract-checked by the fixture conformance test suite (npx vitest run); npm run build must stay green.
| Library | Purpose |
|---|---|
| Vite | Dev server, bundler, CORS proxy |
| React 19 + TypeScript | UI framework |
| @xyflow/react v12 | Node-based canvas |
| Leaflet | Map rendering (MapOutput, SpatialFilter) |
| pdfjs-dist | Client-side PDF text extraction |
| shpjs | Client-side Shapefile parsing |
| Puppeteer | Headless browser for JS-rendered page fetching |
| react-markdown + remark-gfm | Markdown + GFM table rendering in the KCL Assistant chat panel |
| Ollama | Local LLM inference (external; hidden from sidebar) |
No backend. No database. No authentication. All API calls are made directly from the browser (or via the Vite dev proxy for services without permissive CORS). For deployed instances, see the deploy/express-server branch.
