Run the Quaero Explorer
Run the Quaero Explorer demo app and discover Metanno’s collaborative workflow: real‑time syncing, multi‑panel editing, and simple persistence.
Prerequisites
Reuse the same environment you prepared in the New project tutorial. You should already have a working Python environment with Pret, Metanno, and EDS‑NLP installed.
The script downloads the Quaero FrenchMed dataset on first run, so an Internet access is required.
Create a new file named quaero.py in your project and copy the contents from the official example https://github.com/percevalw/metanno/blob/main/examples/quaero.py.
Run the app
from collections import Counter
from pathlib import Path
import edsnlp
from metanno.recipes.explorer import DatasetApp
from pret import component, use_store_snapshot
from pret.ui.markdown import Markdown
from pret.ui.react import div
from pret.ui.simple_dock import Layout, Panel
From a terminal. The server will start at http://localhost:5000.
python quaero.py --port 5000
Or in JupyterLab, run the following cell:
from quaero import app
app()
Code breakdown
Below, we break down the script.
1) Load the data
We first fetch and extracts the dataset if it isn’t already present, and read the BRAT standoff annotations using EDS-NLP. We then build two Python lists: notes and entities with the fields we’ll display and edit.
download_quaero()
data = edsnlp.data.read_standoff(
    DOWNLOAD_DIR / "QUAERO_FrenchMed/corpus",
    span_setter="entities",
    notes_as_span_attribute="cui",
)
notes = [
    {
        "note_id": doc._.note_id,
        "note_text": doc.text,
        "seen": False,
    }
    for doc in data
]
entities = [
    {
        "id": f"#-{doc._.note_id}-{e.start_char}-{e.end_char}-{e.label_}",
        "note_id": doc._.note_id,
        "text": str(e),
        "begin": e.start_char,
        "end": e.end_char,
        "label": e.label_,
        "concept": e._.cui,
    }
    for doc in data
    for e in sorted(doc.spans["entities"])
]
2) Check unique entity IDs
The Table component requires unique IDs for each row. We check if the entity IDs are unique and raise an error if not, or deduplicate them automatically.
if deduplicate:
    entities = list({v["id"]: v for v in entities}.values())
else:
    counter = Counter(e["id"] for e in entities)
    if any(count > 1 for count in counter.values()):
        raise ValueError(
            "Duplicate IDs found in the dataset: "
            + ", ".join(f"{id_} (x{n})" for id_, n in counter.items() if n > 1)
        )
3) Configure labels
We compute a stable list of labels, assign a color for each, and auto‑pick a one‑letter keyboard shortcut per label.
all_labels = list(dict.fromkeys(e["label"] for e in entities))
shortcuts = set()
labels_config = {}
for i, label in enumerate(all_labels):
    labels_config[label] = {}
    if i < len(PALETTE):
        labels_config[label]["color"] = PALETTE[i]
    letter = next(c for c in label.lower() if c not in shortcuts)
    shortcuts.add(letter)
    labels_config[label]["shortcut"] = letter
4) Instantiate the manager
We create a DatasetApp (a ready‑to‑customize recipe, you may also see it referred to as a "Dataset Explorer" in the docs). Under the hood, it composes Metanno components such as Table, AnnotatedText, buttons with a bit of app logic.
app = DatasetApp(
    {
        "notes": notes,
        "entities": entities,
    },
    sync=save_path,
)
5) Build the views
We define the views that will be rendered in the app. Each view is a component that displays a specific part of the data.
notes_view = app.render_table(
    view_name="notes",
    store_key="notes",
    pkey_column="note_id",
    id_columns=["note_id"],
    first_columns=["note_id", "seen", "note_text"],
    editable_columns=[],
    categorical_columns=[],
    hidden_columns=[],
    style={"--min-notebook-height": "300px"},
)
entities_view = app.render_table(
    view_name="entities",
    store_key="entities",
    pkey_column="id",
    first_columns=["id", "note_id", "text", "label", "concept", "begin", "end"],
    id_columns=["id", "note_id"],
    editable_columns=["label", "concept"],
    categorical_columns=["label", "concept"],
    style={"--min-notebook-height": "300px"},
)
note_text_view = app.render_text(
    view_name="note_text",
    # Where to look for text data in the app data
    store_text_key="notes",
    text_column="note_text",
    text_pkey_column="note_id",
    # Where to look for spans data in the app data
    store_spans_key="entities",
    spans_pkey_column="id",
    style={"--min-notebook-height": "300px"},
    labels=labels_config,
)
6) Assemble everything
Panels can be resized, rearranged (drag the tab handles), or hidden by docking into another panel’s tab bar. The note panel header is customized to display the current note id.
app_state = app.state
app_data = app.data
@component
def NoteHeader():
    doc_idx = use_store_snapshot(app_state["notes"])["last_idx"]
    if doc_idx is None:
        return "Note"
    return f"Note ({app_data['notes'][doc_idx]['note_id']})"
layout = div(
    Layout(
        Panel(div(Markdown(DESC), style={"margin": "10px"}), key="Description"),
        Panel(notes_view, header="Notes", key="notes"),
        Panel(entities_view, header="Entities", key="entities"),
        Panel(note_text_view, header=NoteHeader(), key="note_text"),
        # Describe how the panels should be arranged by default
        default_config={
            "kind": "row",
            "children": [
                "Description",
                {"children": ["notes", "entities"]},
                "note_text",
            ],
        },
        collapse_tabs_on_mobile=["note_text", "Description", "notes", "entities"],
    ),
    style={
        "background": "var(--joy-palette-background-level2, #f0f0f0)",
        "width": "100%",
        "height": "100%",
        "minHeight": "300px",
        "--sd-background-color": "transparent",
    },
)
return layout
7) Render or serve
You can either serve it or display it in a notebook, following the instructions in the previous section.
JupyterLab Tabs
In notebooks, Pret layouts cannot be "mixed" with JupyterLab’s own UI system, and will always be embedded in a single JupyterLab tab. You may prefer displaying specific views in separate cells.
Simply display the variables notes_view, entities_view, note_text_view (the return values of app.render_text and app.render_table) in different cells.
Syncing, collaboration, and saving
Metanno (via Pret) can sync app state across clients and optionally persist it.
Live sync only (no persistence)
Pass sync=True when creating the app to enable real‑time collaboration without saving to disk:
app = DatasetApp(
    {
        "notes": notes,
        "entities": entities,
    },
    sync=True,
)
Open the same notebook twice or the same app URL in two tabs: edits in one tab are mirrored to the other.
Live sync and saving to a file
Provide a file path to append every change to an on‑disk log:
app = DatasetApp(
    {
        "notes": notes,
        "entities": entities,
    },
    sync="quaero.bin",
)
Now changes are saved on disk, and multiple servers/kernels can collaborate by pointing to the same file.
What is the saved format?
It’s a compact, binary, append‑only log of user mutations: you cannot read it directly.
To inspect it, you can create store synced with the file and read current state.
from pret.store import create_store
store = create_store(sync="quaero.bin")
pure_py_object = store.to_py()
To export to other formats, write a small exporter from your in‑memory data (app.data). We recommend using EDS‑NLP data connectors since it supports several data formats and schemas.