"""app_rai.py – RAI Metadata Editor for Croissant JSON-LD files. Load a Croissant JSON-LD file, inspect which Responsible AI metadata fields are present, fill in the missing ones, and export an updated file ready for NeurIPS submission. """ import gradio as gr import json import os import tempfile # Patch gradio_client bug: _json_schema_to_python_type receives a bool when # a JSON schema uses "additionalProperties": false, causing "argument of type # 'bool' is not iterable" on HuggingFace Spaces. import gradio_client.utils as _gc_utils _orig_json_schema_to_python_type = _gc_utils._json_schema_to_python_type def _safe_json_schema_to_python_type(schema, defs=None): if not isinstance(schema, dict): return "any" return _orig_json_schema_to_python_type(schema, defs) _gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type # --------------------------------------------------------------------------- # RAI field definitions # --------------------------------------------------------------------------- RAI_FIELD_DEFS = [ { "key": "rai:dataLimitations", "title": "Data Limitations", "short_help": ( "Describe known constraints on the dataset's applicability: " "distributional gaps, underrepresented populations, data quality " "issues, or domain restrictions. Also list any uses for which this " "dataset is explicitly not recommended." ), "long_help": ( "Every dataset reflects choices — about scope, collection method, and context — that constrain where it can be safely applied. " "This section asks you to make those constraints explicit and visible to downstream users\n\n" "Document the specific boundaries of your dataset: the tasks it was not designed for, the conditions under which it is expected to fail, " "and any use cases where deployment should be strictly avoided. Think about geographic coverage, the modalities captured, the population represented, " "and the conditions of data collection. For example, a medical imaging benchmark evaluated only on European clinical data should explicitly state " "it cannot be generalized to other healthcare systems; a UAV localization dataset collected in Germany and the United States must flag that its " "performance will degrade in regions, scene types, or conditions outside those covered.\n\n" "Pay particular attention to use cases where misapplication carries real harm. Benchmarks designed for model evaluation are routinely misused for " "training; datasets covering safety-critical domains like clinical imaging are sometimes taken as proxies for deployment readiness. If your dataset " "is not suitable for a specific application, say so explicitly — a sentence stating \"this dataset is strictly not recommended for clinical deployment " "or commercial applications\" protects both your work and downstream users." ), "type": "text", "placeholder": ( "e.g. This dataset is limited to English-language text collected from " "Western news outlets and may not generalise to other languages or " "cultural contexts. It is not recommended for use in safety-critical " "applications without additional validation." ), }, { "key": "rai:dataBiases", "title": "Data Biases", "short_help": ( "Describe any known or suspected biases in the data, including " "selection bias, label bias, or demographic skew. Indicate which " "population groups or scenarios may be over- or under-represented, " "and how this may affect model behaviour." ), "long_help": ( "Bias in a dataset does not mean the data is wrong — it means the data reflects a particular slice of the world. " "This section asks you to identify which slice, and what that implies for downstream applications, such as models trained using this data.\n\n" "Document any known or suspected systematic skew in your data: who is over- or under-represented, which languages or geographic regions dominate, " "whether annotation was performed by a homogeneous group, and how any of these factors might affect model behaviour in practice. Be specific. " "Rather than stating \"the dataset may contain demographic bias,\" describe the actual distribution — for instance, that annotators were predominantly " "from the United States, United Kingdom, and Canada, with a majority holding higher education qualifications, and that this skew is reflected in quality " "judgements. Similarly, if a dataset draws from a single institutional source, document how that source's practices and norms may have shaped what is — " "and is not — present in the data.\n\n" "Where the bias is structural — such as a naturally long-tailed distribution of rare conditions, or a deliberate overrepresentation of one collection " "environment — explain whether the imbalance was intentional and what effect it is expected to have on downstream use. Synthetic datasets require " "particular care here. Generators — including large language models — inherit and can amplify the biases of their training data. Document the known " "limitations of the generator, any demographic or cultural skews in its outputs, and whether the synthetic distribution accurately represents the target population." ), "type": "text", "placeholder": ( "e.g. The dataset is sourced entirely from Eurorad, introducing " "geographic and demographic bias towards European patients. " "Rare conditions affecting non-European populations are " "under-represented." ), }, { "key": "rai:personalSensitiveInformation", "title": "Personal & Sensitive Information", "short_help": ( "Indicate whether the dataset contains personal or sensitive " "information. Select all that apply: Gender, Socio-economic status, " "Geography, Language, Age, Culture, Experience or Seniority, Health " "or medical data, Political or religious beliefs, Others (please " "specify). If none is present, please state it explicitly." ), "long_help": ( "This section requires you to take stock of any personal and sensitive information present in your dataset — whether it was collected intentionally " "or incidentally. For each sensitive category present, state it explicitly. The relevant categories include: gender, age, geographic location, language, " "culture, socio-economic status, health and medical data, and political or religious beliefs. If the dataset contains absolutely none of these, state that explicitly too.\n\n" "Beyond listing what is present, document the legal and ethical basis for its inclusion. For datasets containing medical data, confirm whether ethics " "review was conducted and what the outcome was. For datasets derived from user-generated content, acknowledge that personal disclosures may be embedded " "in open-ended entries even when no direct identifiers were collected. If privacy-preserving measures were applied — de-identification, downscaling of " "images, suppression of absolute geolocation, or similar — describe those measures and what residual risk, if any, remains after their application." ), "type": "text", "placeholder": ( "e.g. The dataset contains the following sensitive categories: " "Health or medical data (radiology images and clinical reports). " "All data has been de-identified in accordance with HIPAA guidelines." ), }, { "key": "rai:dataUseCases", "title": "Data Use Cases", "short_help": ( "Describe what real-world concept or phenomenon the dataset is " "intended to measure or represent (e.g., sentiment, toxicity, medical " "diagnosis). List the use cases for which the dataset has been " "validated, citing supporting evidence. Note any use cases where this " "validation does not exist or may not hold." ), "long_help": ( "A dataset is only as reliable as the tasks it was actually designed and validated to support. This section asks you to define those tasks precisely, " "and to be honest about the boundaries of what has been tested. Describe what real-world concept or capability your dataset is intended to measure, and " "provide evidence that it does so reliably. Evidence might include: published benchmark results across multiple models, expert validation of ground-truth " "annotations, human baselines, or independent auditing. For each validated task, state the evaluation metrics used and the conditions under which validity " "has been established.\n\n" "Equally important is what you have not validated. Datasets are frequently applied to tasks for which no validity evidence exists — a localization benchmark " "repurposed for 3D reconstruction, or a diversity measurement corpus repurposed for safety red-teaming. List these out-of-scope use cases explicitly, even " "when they might seem like natural extensions of your dataset's core purpose. If validity has only been established for specific subpopulations, geographic " "contexts, or input conditions, make those boundaries clear. For synthetic datasets, also report resemblance metrics (how closely the synthetic data reflects " "the real-world distribution it models) and utility metrics (evidence that models trained or evaluated on it perform comparably to those using real data). " "Without both, fitness for purpose remains unverified." ), "type": "text", "placeholder": ( "e.g. This dataset evaluates vision-language models on rare anomaly " "localisation in brain MRI. Validated use cases: zero-shot evaluation " "of large multimodal models (see [paper]). Not validated for clinical " "decision support or fine-tuning on production systems." ), }, { "key": "rai:dataSocialImpact", "title": "Data Social Impact", "short_help": ( "Discuss the potential positive and negative societal effects of using " "this dataset, including risks of misuse, fairness implications for " "specific communities, and any mitigations put in place (e.g., access " "restrictions, usage terms, or gated release)." ), "long_help": ( "Releasing a dataset is a consequential act. This section asks you to think through those consequences — the benefits you intend, the risks you foresee, " "and the measures you have taken to mitigate harm.\n" "Start with the positive case: what does this dataset enable that was not possible before, and who stands to benefit? Then consider the risks. These include " "misuse (applying the dataset in ways it was not designed for), over-reliance (treating benchmark performance as a proxy for deployment readiness), and " "distributional harm (building on data that systematically excludes or misrepresents specific communities). For datasets touching safety-critical domains, " "consider what the human cost of a failure would be, and whether your release could accelerate premature deployment.\n\n" "Finally, describe what you have done to mitigate these risks. Concrete mitigations include: restricting use via a non-commercial or share-alike license, " "designing the dataset as evaluation-only to prevent training misuse, applying access controls or gated release, publishing a full limitations document, or " "requiring a usage agreement. The goal is not to eliminate all risk — that is rarely possible — but to demonstrate that you have thought carefully about the " "implications of your work and taken reasonable steps to protect against foreseeable harms." ), "type": "text", "placeholder": ( "e.g. Positive impact: enables rigorous evaluation of AI systems on " "rare medical conditions that are often ignored in mainstream benchmarks. " "Risks: misuse for clinical diagnosis without validation. " "Mitigation: the dataset is released under a non-commercial research " "licence and requires signing a data use agreement." ), }, { "key": "rai:hasSyntheticData", "title": "Contains Synthetic Data", "short_help": ( "A boolean indicating the presence of synthetic data, defined as " "'artificially generated data designed to replicate the statistical " "properties of real data using purpose-built models or algorithms'. " "If yes, describe the generation process in the Provenance Activities " "field below." ), "long_help": ( "Switch to 'yes'if your dataset includes any synthetically generated content — whether fully synthetic or a real-data mixture augmented through " "generative processes. Flagging this field is the entry point, not the end point: if set to true, you are strongly encouraged to document the synthetic " "generation process in detail. This means identifying the seed data used, naming the specific generator (including the " "model or algorithm, and version where applicable), and describing how the synthetic output was validated. Leaving the flag set without complementary " "documentation significantly limits the usefulness of this information for downstream users." ), "type": "radio", "placeholder": None, }, { "key": "prov:wasDerivedFrom", "title": "Source Datasets", "short_help": ( "The URI(s) of the dataset(s) from which the present dataset is " "derived. Can be many URIs from different data sources. For synthetic " "data, point to the seeds used." ), "long_help": ( "If your dataset builds on existing data, this section documents its lineage — the \"parent\" datasets from which it inherits both content and constraints. " "For each source dataset, provide at least a stable URL. Optionally you can provide its name, the publishing organisation, and the license under which it is " "made available. This is not merely a citation formality: license compatibility is a legal requirement, and inherited biases or restrictions travel with the " "data. A dataset derived from a CC BY-NC-SA source, for instance, carries non-commercial restrictions into any derivative work. If your dataset incorporates " "multiple sources with different licenses, document each one and explain how you have ensured compatibility.\n\n" "For datasets generated through data augmentation, synthetic generation, or model-assisted construction, identify the seed data used — even if the final " "dataset bears little surface resemblance to the original. Users need to understand what the data was ultimately derived from." ), "type": "derived_from", }, { "key": "prov:wasGeneratedBy", "title": "Provenance Activities", "short_help": ( "Specify the type of activity: Collection, preprocessing, annotation, " "or others. Describe cleaning/filtering steps, collection methods, or " "annotation processes including human teams, synthetic agents, and " "platforms used." ), "long_help": ( "This section documents the \"who\" and the \"what\" of your dataset's creation — the full chain of decisions and actions that produced it. For each major " "activity — data collection, filtering, preprocessing, annotation, quality review — provide a description of what was done and an account of who or what " "performed the work. Agents may be human (research teams, crowdworkers, domain experts) or synthetic (language models used for classification or " "pre-annotation, generation algorithms, automated pipelines). Both require documentation.\n\n" "For human agents, describe the team's institutional affiliations, language background, and geographic location — and if crowdsourcing was used, the " "platform, the prescreening criteria, and the compensation arrangements. For datasets involving synthetic generation, name the generator specifically " "(including model name and version for LLMs, and the prompting strategy applied), and report the resemblance and utility metrics used to validate the " "output. Where human and synthetic agents collaborated — for instance, a language model performing initial labelling that human annotators then reviewed — " "describe that interaction clearly, since the quality and bias of the final annotations depends on both." ), "type": "activities", "placeholder": None, }, ] # Maximum number of source dataset entries in the prov:wasDerivedFrom multi-slot UI MAX_DERIVED_SLOTS = 5 # Maximum number of provenance activity entries in the prov:wasGeneratedBy multi-slot UI MAX_ACTIVITY_SLOTS = 5 # Maximum number of agents per activity MAX_AGENT_SLOTS = 4 # JSON-LD context namespaces to inject when missing REQUIRED_CONTEXT: dict[str, str] = { "rai": "http://mlcommons.org/croissant/RAI/", "prov": "http://www.w3.org/ns/prov#", "sc": "https://schema.org/", } # Mapping from human-readable activity type labels to Wikidata URIs ACTIVITY_TYPE_MAP: dict[str, str] = { "Data Collection": "https://www.wikidata.org/wiki/Q4929239", "Annotation": "https://www.wikidata.org/wiki/Q109719325", "Data preprocessing": "https://www.wikidata.org/wiki/Q5227332", "Filtering (cleansing)": "https://www.wikidata.org/wiki/Q1172378", "Quality review": "https://www.wikidata.org/wiki/Q3306762", } # Reverse map: URI -> label (for loading existing JSON) _ACTIVITY_TYPE_REVERSE: dict[str, str] = {v: k for k, v in ACTIVITY_TYPE_MAP.items()} # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _field_header_html(title: str, short_help: str, status: str) -> str: """status: 'present' | 'user_filled' | 'missing'""" if status == "present": badge = '✓ Already present' elif status == "user_filled": badge = '✓ OK' else: badge = '⚠ Missing' return ( f'
' f'
' f' {title}' f' {badge}' f'
' f'
{short_help}
' f'
' ) def _summary_html(present_count: int, total: int) -> str: missing = total - present_count if missing == 0: color, msg = "#4caf50", f"✅ All {total} RAI metadata fields are present." else: plural = "s are" if missing > 1 else " is" color = "#ff9800" msg = ( f"⚠ {missing} of {total} RAI metadata field{plural} missing. " "Please fill them in below, then export." ) return ( f'
' f'{msg}
' ) def _update_context(result: dict) -> None: """Ensure @context contains the required RAI and PROV namespaces.""" ctx = result.get("@context") if ctx is None: result["@context"] = dict(REQUIRED_CONTEXT) return if isinstance(ctx, str): # Preserve the original URI string; add our namespaces in a list result["@context"] = [ctx, dict(REQUIRED_CONTEXT)] return if isinstance(ctx, list): # Find the first dict entry and add missing namespaces to it for item in ctx: if isinstance(item, dict): for prefix, uri in REQUIRED_CONTEXT.items(): if prefix not in item: item[prefix] = uri return # No dict entry found — append one ctx.append(dict(REQUIRED_CONTEXT)) return if isinstance(ctx, dict): for prefix, uri in REQUIRED_CONTEXT.items(): if prefix not in ctx: ctx[prefix] = uri def _make_time_picker_html(xsd_val: str, ai: int) -> str: """Render a datetime-local picker that syncs its value to the hidden backing textbox.""" dt_local = "" if xsd_val: s = str(xsd_val).strip() if "T" in s: dt_local = s[:16] # YYYY-MM-DDTHH:MM required by datetime-local js = ( f"(function(el){{" f"var dt=el.value;" f"var xsd=dt?dt+':00Z':'';" f"var tb=document.querySelector('#act-time-tb-{ai} input,#act-time-tb-{ai} textarea');" f"if(tb){{tb.value=xsd;tb.dispatchEvent(new Event('input',{{bubbles:true}}));}}" f"}})(this)" ) return ( f'
' f'' f'' f'
' ) # --------------------------------------------------------------------------- # App CSS # --------------------------------------------------------------------------- # Font is loaded via a tag injected into the page (see create_ui). # @import is not allowed in Gradio 6's Constructable Stylesheets. APP_CSS = """ /* Page background — override the Gradio theme variable so lavender propagates everywhere */ :root { --body-background-fill: rgb(240, 235, 248); } html, body, .app, .main { background-color: rgb(240, 235, 248) !important; } .gradio-container { max-width: 820px !important; margin: 0 auto !important; padding-top: 20px !important; background-color: rgb(240, 235, 248) !important; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; } /* Apply Inter to all Gradio text elements */ .gradio-container * { font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; } /* ALL Gradio structural/layout containers: fully transparent, no border or shadow. Card appearance is controlled exclusively via named classes (.upload-card, .rai-field-card, .license-card, .derived-slot-card). This also removes the Gradio 6 .gradio-group default border that was creating a card-inside-a-card artefact on .rai-field-card elements. */ .wrap, .contain, .block, .form, .padded, .gr-group, .gr-box, .gr-panel, .gradio-group, .gradio-column, .gradio-row, .gradio-accordion, .styler { background-color: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; } /* Upload section card — outermost wrapper only */ .upload-card:not(.upload-card .upload-card) { border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 12px !important; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08) !important; background: white !important; padding: 16px !important; margin-bottom: 16px !important; } .upload-card .upload-card { background: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; } .progress-status { font-style: italic; font-size: 0.9em; color: var(--body-text-color-subdued); padding: 8px 0; text-align: center; background: none !important; border: none !important; } /* Summary bar */ .rai-summary { padding: 10px 16px; border-radius: 8px; background: white; border: 1px solid rgba(0, 0, 0, 0.10); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06); margin-bottom: 20px; font-size: 0.95em; } /* Per-field card — outermost wrapper only. Gradio 6 renders gr.Group as two nested divs that both receive the same elem_classes, so we must suppress card styling on the inner duplicate. */ .rai-field-card:not(.rai-field-card .rai-field-card) { border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 12px !important; padding: 18px 20px 14px 20px; margin-bottom: 16px; background: white !important; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08) !important; } .rai-field-card .rai-field-card { background: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; margin: 0 !important; } /* Field header */ .rai-field-header { margin-bottom: 8px; } .rai-title-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; } .rai-title { font-size: 1.2em; font-weight: 600; } .rai-badge { display: inline-block; padding: 2px 9px; border-radius: 12px; font-size: 0.75em; font-weight: 600; color: white !important; white-space: nowrap; } .badge-present { background-color: #4caf50; } .badge-ok { background-color: #4caf50; } .badge-missing { background-color: #ff9800; } .rai-short-help { font-size: 1em; color: var(--body-text-color); line-height: 1.5; margin-top: 2px; } /* Accordion: lavender underlined link, no arrow icon */ .rai-more-info .label-wrap { font-size: 0.85em !important; color: #7c5cbf !important; text-decoration: underline !important; cursor: pointer !important; } .rai-more-info .label-wrap .icon { display: none !important; } .rai-more-info .label-wrap span:first-child::before { content: '' !important; } /* Add source dataset button */ button.add-derived-btn { background-color: #6366f1 !important; color: white !important; border: none !important; border-radius: 8px !important; } button.add-derived-btn:hover { background-color: #4f46e5 !important; } /* Export section separator */ .export-section { border-top: 1px solid var(--border-color-primary, rgba(128,128,128,0.2)); padding-top: 20px; margin-top: 8px; } /* License section — visually distinct from RAI cards */ .license-divider { border-top: 2px solid var(--border-color-primary, rgba(128,128,128,0.25)); margin: 32px 0 20px 0; } .license-note { font-size: 0.88em; color: var(--body-text-color); padding: 10px 14px; background: white; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.10); border-left: 3px solid #5c85d6; margin-bottom: 14px; line-height: 1.5; } .license-card:not(.license-card .license-card) { border: 1px solid #5c85d6 !important; border-radius: 12px !important; padding: 18px 20px 14px 20px; margin-bottom: 16px; background: white !important; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08) !important; } .license-card .license-card { background: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; margin: 0 !important; } /* Core metadata section hidden by default; shown only when a field is missing */ #rai-license-section { display: none; } /* Individual core field rows hidden by default; shown selectively via style injection */ #core-field-name, #core-field-url, #core-field-license { display: none; } /* Form and export sections hidden by default; revealed by a style-tag injected via a gr.HTML leaf component once a file is loaded. */ #rai-main-sections { display: none; } /* Also hidden by default; revealed alongside the form. */ #rai-main-sections-export { display: none; } /* Source dataset multi-entry slots — all hidden by default; shown via style injection */ #derived-slot-0, #derived-slot-1, #derived-slot-2, #derived-slot-3, #derived-slot-4 { display: none; } .derived-from-slot { margin-top: 6px; } /* Slot card: white background, subtle border — outer card only */ .derived-slot-card:not(.derived-slot-card .derived-slot-card), .derived-slot-card:not(.derived-slot-card .derived-slot-card) > .form, .derived-slot-card:not(.derived-slot-card .derived-slot-card) > div { background: white !important; box-shadow: none !important; } .derived-slot-card:not(.derived-slot-card .derived-slot-card) { border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 10px !important; padding: 12px 14px 8px 14px !important; margin-bottom: 8px !important; } /* Inner duplicate card: nullify */ .derived-slot-card .derived-slot-card { background: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; margin: 0 !important; } .derived-slot-label { font-size: 0.88em; font-weight: 600; color: var(--body-text-color); margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border-color-primary, rgba(128,128,128,0.15)); } .derived-remove-btn { margin-top: 4px !important; font-size: 0.8em !important; color: #c0392b !important; border-color: #c0392b !important; } /* Activity and agent slot hidden by default */ #activity-slot-0,#activity-slot-1,#activity-slot-2,#activity-slot-3,#activity-slot-4 { display: none; } #activity-0-agent-0,#activity-0-agent-1,#activity-0-agent-2,#activity-0-agent-3, #activity-1-agent-0,#activity-1-agent-1,#activity-1-agent-2,#activity-1-agent-3, #activity-2-agent-0,#activity-2-agent-1,#activity-2-agent-2,#activity-2-agent-3, #activity-3-agent-0,#activity-3-agent-1,#activity-3-agent-2,#activity-3-agent-3, #activity-4-agent-0,#activity-4-agent-1,#activity-4-agent-2,#activity-4-agent-3 { display: none; } /* Activity slot card */ .activity-slot-card:not(.activity-slot-card .activity-slot-card) { border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 10px !important; padding: 12px 14px 8px 14px !important; margin-bottom: 8px !important; background: white !important; box-shadow: none !important; } .activity-slot-card .activity-slot-card { background: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; margin: 0 !important; } /* Agent slot card */ .agent-slot-card:not(.agent-slot-card .agent-slot-card) { border: 1px solid rgba(99, 102, 241, 0.25) !important; border-radius: 8px !important; padding: 10px 12px 6px 12px !important; margin-bottom: 6px !important; background: #f9f9ff !important; box-shadow: none !important; } .agent-slot-card .agent-slot-card { background: transparent !important; border: none !important; box-shadow: none !important; border-radius: 0 !important; padding: 0 !important; margin: 0 !important; } .activity-slot-label { font-size: 0.88em; font-weight: 600; color: var(--body-text-color); margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border-color-primary, rgba(128,128,128,0.15)); } .agent-slot-label { font-size: 0.82em; font-weight: 600; color: #6366f1; margin-bottom: 6px; } .agents-section-label { font-size: 0.85em; font-weight: 600; color: var(--body-text-color-subdued); margin: 10px 0 4px 0; text-transform: uppercase; letter-spacing: 0.04em; } .activity-remove-btn { margin-top: 4px !important; font-size: 0.8em !important; color: #c0392b !important; border-color: #c0392b !important; } .agent-remove-btn { margin-top: 2px !important; font-size: 0.75em !important; color: #c0392b !important; border-color: #c0392b !important; } /* Hidden backing textbox for the datetime picker */ .act-time-hidden { display: none !important; } /* RAI dropdowns: visible border */ .rai-dropdown .wrap { border: 1px solid rgba(0, 0, 0, 0.28) !important; border-radius: 6px !important; background: white !important; } button.add-activity-btn { background-color: #6366f1 !important; color: white !important; border: none !important; border-radius: 8px !important; margin-top: 6px !important; } button.add-activity-btn:hover { background-color: #4f46e5 !important; } button.add-agent-btn { background-color: rgba(99, 102, 241, 0.12) !important; color: #4f46e5 !important; border: 1px solid rgba(99, 102, 241, 0.3) !important; border-radius: 6px !important; font-size: 0.82em !important; margin-top: 4px !important; } button.add-agent-btn:hover { background-color: rgba(99, 102, 241, 0.2) !important; } """ # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- def create_ui(): with gr.Blocks(theme=gr.themes.Soft(), css=APP_CSS) as app: # Load Inter font via a tag — @import is forbidden in Gradio 6 # Constructable Stylesheets and would break all event listeners. gr.HTML( '' '' '' ) # ── Header ──────────────────────────────────────────────────────────── gr.HTML( "

" ) gr.Markdown("# 🥐 Croissant RAI Metadata Editor") gr.Markdown( "Upload a Croissant JSON file to check which Responsible AI " "metadata fields are present. Fill in the missing fields and export " "an updated file ready for NeurIPS submission. " "[Read the NeurIPS dataset submission requirements]" "(https://neurips.cc/Conferences/2026/EvaluationsDatasetsHosting).") gr.Markdown( "A detailed description of the require RAI metadata with concrete xamples can be found in the [guidelines for RAI attributes]" "(https://docs.google.com/document/d/179uI7VYEbm9KbN0HBlktjX3x6Mc5Pj-HYtegfvziqtM)" ) # ── Upload section ──────────────────────────────────────────────────── with gr.Group(elem_classes=["upload-card"]): file_input = gr.File( label="Upload Croissant JSON-LD File", show_label=False, file_types=[".json", ".jsonld"], ) load_btn = gr.Button("Load File", variant="primary") status_html = gr.HTML( '
Upload a Croissant file to get started.
' ) # ── Shared state ────────────────────────────────────────────────────── json_data_state = gr.State(None) # Tracks which fields were present in the originally loaded JSON field_presence_state = gr.State([False] * len(RAI_FIELD_DEFS)) # ── Visibility toggle (leaf HTML component, safe as Gradio 6 output) ─ # Updating this with a " _all_outputs = ( [json_data_state, status_html, dataset_info_md, summary_html, field_presence_state, visibility_toggle] + field_header_htmls + field_inputs + [name_header_html, name_present_state, name_input, url_header_html, url_present_state, url_input, license_header_html, license_present_state, license_input, core_fields_note, core_fields_toggle] ) def _empty_updates(): updates = [] for fd in RAI_FIELD_DEFS: if fd["type"] == "radio": updates.append(gr.update(value="No, this dataset does not contain synthetic data")) elif fd["type"] in ("derived_from", "activities"): updates.append([]) # gr.State: empty entries list else: updates.append(gr.update(value="")) return updates _no_presence = [False] * len(RAI_FIELD_DEFS) def on_load(file): import traceback as _tb def _safe_return(msg, is_error=False): color = "color:#c0392b;" if is_error else "" return ( [ None, f'
{msg}
', "", "", _no_presence, _STYLE_HIDDEN, ] + [gr.update(value="") for _ in RAI_FIELD_DEFS] + _empty_updates() + [gr.update(value=""), False, gr.update(value=""), # name gr.update(value=""), False, gr.update(value=""), # url gr.update(value=""), False, gr.update(value=""), # license gr.update(value=""), ""] # note, toggle ) if file is None: return _safe_return("Upload a Croissant file to get started.") try: # Gradio 6 (type='filepath') returns a plain str; older versions # returned a FileData object (.path) or a file wrapper (.name). if isinstance(file, str): file_path = file else: file_path = getattr(file, "path", None) or getattr(file, "name", None) if not file_path: return _safe_return("❌ Could not read the uploaded file path.", True) with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) except json.JSONDecodeError as e: return _safe_return(f"❌ Invalid JSON: {e}", True) except OSError as e: return _safe_return(f"❌ Cannot open file: {e}", True) except Exception as e: _tb.print_exc() return _safe_return(f"❌ Unexpected error: {e}", True) try: name = data.get("name", "Unnamed dataset") presence = [fd["key"] in data for fd in RAI_FIELD_DEFS] present_count = sum(presence) total = len(RAI_FIELD_DEFS) header_updates = [] input_updates = [] for fd, is_present in zip(RAI_FIELD_DEFS, presence): key = fd["key"] if fd["type"] == "derived_from": raw = data.get(key, []) entries = [] if isinstance(raw, list): for entry in raw: if isinstance(entry, dict): entries.append(entry) elif isinstance(raw, str) and raw.strip(): entries.append({"@id": raw.strip()}) has_url = bool(entries) status = "present" if (is_present and has_url) else "missing" header_updates.append( gr.update(value=_field_header_html(fd["title"], fd["short_help"], status)) ) input_updates.append(entries) # update gr.State with entry dicts continue if fd["type"] == "activities": raw = data.get(key, []) acts = [] if isinstance(raw, list): for entry in raw: if isinstance(entry, dict): acts.append(entry) has_any = bool(acts) status = "present" if (is_present and has_any) else "missing" header_updates.append( gr.update(value=_field_header_html(fd["title"], fd["short_help"], status)) ) input_updates.append(acts) # update gr.State with activity dicts continue if is_present: raw = data.get(key, "") val_str = raw if isinstance(raw, str) else json.dumps(raw, ensure_ascii=False) status = "present" if fd["type"] == "radio" or len(val_str.strip()) >= 10 else "user_filled" else: status = "missing" header_updates.append( gr.update(value=_field_header_html(fd["title"], fd["short_help"], status)) ) if fd["type"] == "radio": val_bool = bool(data.get(key, False)) input_updates.append(gr.update( value="Yes, this dataset contains synthetic data" if val_bool else "No, this dataset does not contain synthetic data" )) else: raw = data.get(key, "") val = raw if isinstance(raw, str) else json.dumps(raw, ensure_ascii=False) input_updates.append(gr.update(value=val)) # Core metadata fields (name, url, license) def _core_val(key): raw = data.get(key, "") return raw if isinstance(raw, str) else json.dumps(raw, ensure_ascii=False) name_val = _core_val("name") url_val = _core_val("url") lic_val = _core_val("license") name_present = bool(name_val.strip()) url_present = bool(url_val.strip()) lic_present = bool(lic_val.strip()) name_header = _field_header_html("Name", "The name of the dataset.", "present" if name_present else "missing") url_header = _field_header_html("URL", "The canonical URL of the dataset.", "present" if url_present else "missing") lic_header = _field_header_html("License", "The name or URL of the licence governing your dataset. Datasets should have a clear license that enables the intended use of the data. Where possible, use open licenses that allow reuse and reproducibility. However, when the dataset contains sensitive data or stricter licensing is unavoidable, please select an appropriate license that is as open as possible given the constraints.", "present" if lic_present else "missing") # Build style: show section if any field is missing; show individual sub-sections shown_ids = [] if not name_present: shown_ids.append("name") if not url_present: shown_ids.append("url") if not lic_present: shown_ids.append("license") n_missing = len(shown_ids) if n_missing == 1: note_msg = "⚠ 1 core Croissant metadata field is missing from your file. Please fill it in as well." else: note_msg = f"⚠ {n_missing} core Croissant metadata fields are missing from your file. Please fill them in as well." core_note_html = f'
{note_msg}
' if shown_ids: css = "#rai-license-section{display:block!important}" + "".join( f"#core-field-{fid}{{display:block!important}}" for fid in shown_ids ) core_style = f"" else: core_style = "" return ( [ data, '
✅ File loaded successfully.
', f"## {name}", _summary_html(present_count, total), presence, _STYLE_SHOWN, ] + header_updates + input_updates + [gr.update(value=name_header), name_present, gr.update(value=name_val), gr.update(value=url_header), url_present, gr.update(value=url_val), gr.update(value=lic_header), lic_present, gr.update(value=lic_val), gr.update(value=core_note_html), core_style] ) except Exception as e: _tb.print_exc() return _safe_return(f"❌ Unexpected error processing file: {e}", True) def on_export(json_data, *all_values): import traceback as _tb def _export_error(msg): return ( None, f'
{msg}
', ) if json_data is None: return _export_error("❌ No file loaded. Please load a Croissant file first.") try: # Inputs: simple RAI fields, then derived-from slot values, # then activity slot values, then core metadata fields n_simple = len([fd for fd in RAI_FIELD_DEFS if fd["type"] not in ("derived_from", "activities")]) n_slots_flat = MAX_DERIVED_SLOTS * 5 n_acts_flat = MAX_ACTIVITY_SLOTS * (4 + MAX_AGENT_SLOTS * 3) simple_values = all_values[:n_simple] slot_values_flat = all_values[n_simple : n_simple + n_slots_flat] act_values_flat = all_values[n_simple + n_slots_flat : n_simple + n_slots_flat + n_acts_flat] _core_base = n_simple + n_slots_flat + n_acts_flat name_value = str(all_values[_core_base + 0] or "").strip() if len(all_values) > _core_base + 0 else "" url_value = str(all_values[_core_base + 1] or "").strip() if len(all_values) > _core_base + 1 else "" license_value = str(all_values[_core_base + 2] or "").strip() if len(all_values) > _core_base + 2 else "" result = dict(json_data) _update_context(result) # Merge simple RAI fields (definition order, skipping derived_from/activities) _s_idx = 0 for fd in RAI_FIELD_DEFS: if fd["type"] in ("derived_from", "activities"): continue val = simple_values[_s_idx] _s_idx += 1 if fd["type"] == "radio": result[fd["key"]] = str(val or "").startswith("Yes") elif val and str(val).strip(): result[fd["key"]] = str(val).strip() # Merge prov:wasDerivedFrom from slot textboxes entries = [] for i in range(MAX_DERIVED_SLOTS): base = i * 5 name = str(slot_values_flat[base + 0] or "").strip() url = str(slot_values_flat[base + 1] or "").strip() lic = str(slot_values_flat[base + 2] or "").strip() pub_name = str(slot_values_flat[base + 3] or "").strip() pub_url = str(slot_values_flat[base + 4] or "").strip() if not url: continue entry: dict = {"@id": url} if name: entry["prov:label"] = name if lic: entry["sc:license"] = lic if pub_name or pub_url: attr: dict = {} if pub_url: attr["@id"] = pub_url if pub_name: attr["prov:label"] = pub_name entry["prov:wasAttributedTo"] = attr entries.append(entry) if entries: result["prov:wasDerivedFrom"] = entries # Merge prov:wasGeneratedBy from activity slot inputs act_per_slot = 4 + MAX_AGENT_SLOTS * 3 activities = [] for i in range(MAX_ACTIVITY_SLOTS): base = i * act_per_slot act_type = str(act_values_flat[base + 0] or "").strip() act_name = str(act_values_flat[base + 1] or "").strip() act_desc = str(act_values_flat[base + 2] or "").strip() act_time = str(act_values_flat[base + 3] or "").strip() if not act_name and not act_desc: continue activity: dict = {"@type": "prov:Activity"} if act_type: uri = ACTIVITY_TYPE_MAP.get(act_type) activity["prov:type"] = {"@id": uri} if uri else {"@id": act_type} if act_name: activity["prov:label"] = act_name if act_desc: activity["sc:description"] = act_desc if act_time: activity["prov:atTime"] = act_time agents = [] agent_base = base + 4 for j in range(MAX_AGENT_SLOTS): g_base = agent_base + j * 3 agt_type = str(act_values_flat[g_base + 0] or "").strip() agt_name = str(act_values_flat[g_base + 1] or "").strip() agt_desc = str(act_values_flat[g_base + 2] or "").strip() if not agt_name: continue agent: dict = { "@type": f"prov:{agt_type or 'Agent'}", "@id": agt_name.lower().replace(" ", "_").replace("-", "_"), "prov:label": agt_name, } if agt_desc: agent["sc:description"] = agt_desc agents.append(agent) if agents: activity["prov:wasAttributedTo"] = agents activities.append(activity) if activities: result["prov:wasGeneratedBy"] = activities if name_value: result["name"] = name_value if url_value: result["url"] = url_value if license_value: result["license"] = license_value # Write to a temporary file safe_name = ( str(result.get("name", "dataset"))[:50] .replace(" ", "_") .replace("/", "_") .replace("\\", "_") ) fname = f"croissant_rai_{safe_name}.json" tmp_path = os.path.join(tempfile.gettempdir(), fname) with open(tmp_path, "w", encoding="utf-8") as f: json.dump(result, f, indent=2, ensure_ascii=False) return ( tmp_path, '
✅ Export ready. Click the file below to download.
', ) except Exception as e: _tb.print_exc() return _export_error(f"❌ Export failed: {e}") # ── Wire up events ──────────────────────────────────────────────────── # Flat helpers for the derived_from multi-slot section _all_slot_textboxes = [tb for slot in _derived_from_slots for tb in slot] _all_url_slots = [slot[1] for slot in _derived_from_slots] _derived_from_idx = next( i for i, fd in enumerate(RAI_FIELD_DEFS) if fd["type"] == "derived_from" ) def _build_slot_style_from_set(visible_set) -> str: """Return a " def populate_derived_from_slots(entries): """Populate slot textboxes after on_load writes _derived_from_raw_state.""" entries = entries or [] updates = [] for i in range(MAX_DERIVED_SLOTS): if i < len(entries): e = entries[i] pub = e.get("prov:wasAttributedTo") or {} updates.extend([ gr.update(value=e.get("prov:label", "")), gr.update(value=e.get("@id", "")), gr.update(value=e.get("sc:license", "")), gr.update(value=pub.get("prov:label", "") if isinstance(pub, dict) else ""), gr.update(value=pub.get("@id", "") if isinstance(pub, dict) else ""), ]) else: updates.extend([gr.update(value="")] * 5) visible = list(range(min(len(entries), MAX_DERIVED_SLOTS))) updates.append(visible) # _derived_from_visible_set updates.append(_build_slot_style_from_set(visible)) # _derived_from_slot_toggle return updates # ── Activity slot populate helpers (must be defined before load_btn.click) ── _activity_idx = next( i for i, fd in enumerate(RAI_FIELD_DEFS) if fd["type"] == "activities" ) _activity_h = field_header_htmls[_activity_idx] _activity_fd = RAI_FIELD_DEFS[_activity_idx] _all_activity_textboxes = [] for _ai in range(MAX_ACTIVITY_SLOTS): _at, _an, _ad, _atm = _activity_slots[_ai] _all_activity_textboxes.extend([_at, _an, _ad, _atm]) for _gi in range(MAX_AGENT_SLOTS): _gtype, _gname, _gdesc = _agent_slots_per_act[_ai][_gi] _all_activity_textboxes.extend([_gtype, _gname, _gdesc]) def _build_activity_slot_style_from_set(visible_set) -> str: visible_set = visible_set or [] if not visible_set: return "" css = "".join( f"#activity-slot-{i}{{display:block!important}}" for i in visible_set ) return f"" def _build_agent_slot_style_from_set(act_idx: int, visible_set) -> str: visible_set = visible_set or [] if not visible_set: return "" css = "".join( f"#activity-{act_idx}-agent-{j}{{display:block!important}}" for j in visible_set ) return f"" def populate_activities_slots(entries): entries = entries or [] updates = [] for i in range(MAX_ACTIVITY_SLOTS): e = entries[i] if i < len(entries) else {} raw_type = e.get("prov:type", "Data Collection") if isinstance(raw_type, dict): raw_type = _ACTIVITY_TYPE_REVERSE.get(raw_type.get("@id", ""), "Data Collection") act_type = raw_type if raw_type in ACTIVITY_TYPE_MAP else "Data Collection" updates.extend([ gr.update(value=act_type), gr.update(value=e.get("prov:label", "")), gr.update(value=e.get("sc:description", "") or e.get("prov:description", "")), gr.update(value=e.get("prov:atTime", "")), ]) agents_raw = e.get("prov:wasAttributedTo", []) if isinstance(agents_raw, dict): agents_raw = [agents_raw] agents_raw = agents_raw if isinstance(agents_raw, list) else [] for j in range(MAX_AGENT_SLOTS): if j < len(agents_raw): ag = agents_raw[j] atype = ag.get("@type", "prov:Agent").replace("prov:", "") if atype not in ("Agent", "SoftwareAgent"): atype = "Agent" updates.extend([ gr.update(value=atype), gr.update(value=ag.get("prov:label", "")), gr.update(value=ag.get("sc:description", "") or ag.get("prov:description", "") or ag.get("description", "")), ]) else: updates.extend([gr.update(value="Agent"), gr.update(value=""), gr.update(value="")]) visible_acts = list(range(min(len(entries), MAX_ACTIVITY_SLOTS))) updates.append(visible_acts) updates.append(_build_activity_slot_style_from_set(visible_acts)) for i in range(MAX_ACTIVITY_SLOTS): if i < len(entries): ar = entries[i].get("prov:wasAttributedTo", []) if isinstance(ar, dict): ar = [ar] n_agt = len(ar) if isinstance(ar, list) else 0 else: n_agt = 0 vis_agt = list(range(min(n_agt, MAX_AGENT_SLOTS))) updates.append(vis_agt) updates.append(_build_agent_slot_style_from_set(i, vis_agt)) for i in range(MAX_ACTIVITY_SLOTS): e = entries[i] if i < len(entries) else {} updates.append(gr.update(value=_make_time_picker_html(e.get("prov:atTime", ""), i))) return updates _populate_activities_outputs = ( _all_activity_textboxes + [_activity_visible_set, _activity_slot_toggle] + [item for i in range(MAX_ACTIVITY_SLOTS) for item in [_agents_visible_states[i], _agents_slot_toggles[i]]] + _act_time_pickers ) load_btn.click(fn=on_load, inputs=file_input, outputs=_all_outputs).then( fn=populate_derived_from_slots, inputs=[_derived_from_raw_state], outputs=_all_slot_textboxes + [_derived_from_visible_set, _derived_from_slot_toggle], ).then( fn=populate_activities_slots, inputs=[_activity_raw_state], outputs=_populate_activities_outputs, ) # "Add source dataset" button — find next unused slot and show it def on_add_slot(visible_set): visible_set = list(visible_set or []) used = set(visible_set) for i in range(MAX_DERIVED_SLOTS): if i not in used: new_set = visible_set + [i] break else: new_set = visible_set # all slots already used return new_set, _build_slot_style_from_set(new_set) _add_derived_btn.click( fn=on_add_slot, inputs=[_derived_from_visible_set], outputs=[_derived_from_visible_set, _derived_from_slot_toggle], ) # "Remove" button per slot — clear its textboxes and hide it def make_delete_fn(slot_idx: int): def fn(visible_set): new_set = [s for s in (visible_set or []) if s != slot_idx] return ["", "", "", "", "", new_set, _build_slot_style_from_set(new_set)] return fn for _si, (_del_btn, (_sn, _su, _sl, _spn, _spu)) in enumerate( zip(_derived_from_del_btns, _derived_from_slots) ): _del_btn.click( fn=make_delete_fn(_si), inputs=[_derived_from_visible_set], outputs=[_sn, _su, _sl, _spn, _spu, _derived_from_visible_set, _derived_from_slot_toggle], ) # Dynamic badge updates: go green once the user has typed ≥10 characters def make_badge_updater(idx: int, fd: dict): def fn(value, presence_list): plist = presence_list or _no_presence originally_present = plist[idx] if fd["type"] == "radio": user_ok = bool(value) # any selection counts as filled else: user_ok = len(str(value or "").strip()) >= 10 if originally_present: status = "present" elif user_ok: status = "user_filled" else: status = "missing" return gr.update( value=_field_header_html(fd["title"], fd["short_help"], status) ) return fn for _i, (_fd, _inp, _h) in enumerate( zip(RAI_FIELD_DEFS, field_inputs, field_header_htmls) ): if _fd["type"] in ("derived_from", "activities"): continue # badge wired separately _inp.change( fn=make_badge_updater(_i, _fd), inputs=[_inp, field_presence_state], outputs=_h, ) # Badge updater for derived_from: fires when any URL slot changes _derived_from_h = field_header_htmls[_derived_from_idx] _derived_from_fd = RAI_FIELD_DEFS[_derived_from_idx] def make_derived_badge_updater(): def fn(*args): presence_list = args[-1] url_values = args[:-1] plist = presence_list or _no_presence originally_present = plist[_derived_from_idx] user_ok = any(str(u or "").strip() for u in url_values) if originally_present: status = "present" elif user_ok: status = "user_filled" else: status = "missing" return gr.update( value=_field_header_html( _derived_from_fd["title"], _derived_from_fd["short_help"], status ) ) return fn for _url_slot in _all_url_slots: _url_slot.change( fn=make_derived_badge_updater(), inputs=_all_url_slots + [field_presence_state], outputs=_derived_from_h, ) # Badge updaters for core metadata fields def make_core_badge_fn(label, desc, min_len=2): def fn(value, present): if present: status = "present" elif len(str(value or "").strip()) >= min_len: status = "user_filled" else: status = "missing" return gr.update(value=_field_header_html(label, desc, status)) return fn name_input.change( fn=make_core_badge_fn("Name", "The name of the dataset."), inputs=[name_input, name_present_state], outputs=name_header_html, ) url_input.change( fn=make_core_badge_fn("URL", "The canonical URL of the dataset."), inputs=[url_input, url_present_state], outputs=url_header_html, ) license_input.change( fn=make_core_badge_fn("License", "The name or URL of the licence governing your dataset. Datasets should have a clear license that enables the intended use of the data. Where possible, use open licenses that allow reuse and reproducibility. However, when the dataset contains sensitive data or stricter licensing is unavoidable, please select an appropriate license that is as open as possible given the constraints.", min_len=3), inputs=[license_input, license_present_state], outputs=license_header_html, ) # ── Activity event handlers ────────────────────────────────────────── # "Add provenance activity" button def on_add_activity(visible_set): visible_set = list(visible_set or []) used = set(visible_set) for i in range(MAX_ACTIVITY_SLOTS): if i not in used: new_set = visible_set + [i] break else: new_set = visible_set return new_set, _build_activity_slot_style_from_set(new_set) _add_activity_btn.click( fn=on_add_activity, inputs=[_activity_visible_set], outputs=[_activity_visible_set, _activity_slot_toggle], ) # "Remove activity" buttons def make_delete_activity_fn(act_idx: int): def fn(visible_set): new_set = [s for s in (visible_set or []) if s != act_idx] agent_clears = [] for _ in range(MAX_AGENT_SLOTS): agent_clears.extend(["Agent", "", ""]) return ( [list(ACTIVITY_TYPE_MAP.keys())[0], "", "", ""] + agent_clears + [[], ""] + [new_set, _build_activity_slot_style_from_set(new_set)] + [_make_time_picker_html("", act_idx)] ) return fn for _ai_idx in range(MAX_ACTIVITY_SLOTS): _activity_del_btns[_ai_idx].click( fn=make_delete_activity_fn(_ai_idx), inputs=[_activity_visible_set], outputs=( list(_activity_slots[_ai_idx]) + [item for _gi in range(MAX_AGENT_SLOTS) for item in _agent_slots_per_act[_ai_idx][_gi]] + [_agents_visible_states[_ai_idx], _agents_slot_toggles[_ai_idx]] + [_activity_visible_set, _activity_slot_toggle] + [_act_time_pickers[_ai_idx]] ), ) # "Add agent" buttons def make_add_agent_fn(act_idx: int): def fn(visible_set): visible_set = list(visible_set or []) used = set(visible_set) for j in range(MAX_AGENT_SLOTS): if j not in used: new_set = visible_set + [j] break else: new_set = visible_set return new_set, _build_agent_slot_style_from_set(act_idx, new_set) return fn for _ai_idx in range(MAX_ACTIVITY_SLOTS): _agent_add_btns_per_act[_ai_idx].click( fn=make_add_agent_fn(_ai_idx), inputs=[_agents_visible_states[_ai_idx]], outputs=[_agents_visible_states[_ai_idx], _agents_slot_toggles[_ai_idx]], ) # "Remove agent" buttons def make_delete_agent_fn(act_idx: int, agt_idx: int): def fn(visible_set): new_set = [s for s in (visible_set or []) if s != agt_idx] return ( ["Agent", "", ""] + [new_set, _build_agent_slot_style_from_set(act_idx, new_set)] ) return fn for _ai_idx in range(MAX_ACTIVITY_SLOTS): for _gi_idx in range(MAX_AGENT_SLOTS): _agent_del_btns_per_act[_ai_idx][_gi_idx].click( fn=make_delete_agent_fn(_ai_idx, _gi_idx), inputs=[_agents_visible_states[_ai_idx]], outputs=( list(_agent_slots_per_act[_ai_idx][_gi_idx]) + [_agents_visible_states[_ai_idx], _agents_slot_toggles[_ai_idx]] ), ) # Badge updater for activities: fires when any activity name changes _activity_name_slots = [_activity_slots[i][1] for i in range(MAX_ACTIVITY_SLOTS)] def make_activity_badge_updater(): def fn(*args): presence_list = args[-1] name_values = args[:-1] plist = presence_list or _no_presence originally_present = plist[_activity_idx] user_ok = any(str(n or "").strip() for n in name_values) if originally_present: status = "present" elif user_ok: status = "user_filled" else: status = "missing" return gr.update( value=_field_header_html(_activity_fd["title"], _activity_fd["short_help"], status) ) return fn for _act_name_slot in _activity_name_slots: _act_name_slot.change( fn=make_activity_badge_updater(), inputs=_activity_name_slots + [field_presence_state], outputs=_activity_h, ) _simple_field_inputs = [ inp for fd, inp in zip(RAI_FIELD_DEFS, field_inputs) if fd["type"] not in ("derived_from", "activities") ] _all_activity_inputs = [] for _ai in range(MAX_ACTIVITY_SLOTS): _all_activity_inputs.extend(list(_activity_slots[_ai])) for _gi in range(MAX_AGENT_SLOTS): _all_activity_inputs.extend(list(_agent_slots_per_act[_ai][_gi])) export_btn.click( fn=on_export, inputs=[json_data_state] + _simple_field_inputs + _all_slot_textboxes + _all_activity_inputs + [name_input, url_input, license_input], outputs=[export_file, status_html], ) return app if __name__ == "__main__": app = create_ui() app.launch()