From 310cf466cb6f0fdf7a05f986167428ab12f6eb3b Mon Sep 17 00:00:00 2001 From: Zhongyang Li Date: Mon, 18 May 2026 11:44:45 +0000 Subject: [PATCH 01/10] feat(phet_simulations): add new site Adds Flask app, templates, and seed DB for PhET Interactive Simulations (https://phet.colorado.edu/), claiming port slot 40015. Catalog: 98 simulations across 5 subjects (physics, chemistry, math, biology, earth-science), 4 grade levels (elementary, middle, high, university), and 28 languages (incl. 3 RTL scripts). Every primary filter bucket clears the >=20-record threshold. Models: User, Subject, GradeLevel, Language, Simulation, Activity, SavedSimulation. Routes: 17 public + 2 JSON APIs + /_health, all reachable from /. Auth via Flask-Login + bcrypt; saves via CSRF-protected JSON endpoints. Idempotency: every seed_* helper early-returns when its table is populated; verified byte-identical (md5 e094a2ee...) across the control_server reset cycle (rm -rf instance; cp -a instance_seed instance; re-import app). Tested under the exact Dockerfile pin set (Flask 3.1.0, SQLAlchemy 2.0.36, Werkzeug 3.1.3). Per the port-slot convention, also: - websyn_start.sh: append to SITES, bump 15 -> 16 in startup messages - control_server.py: append 'phet_simulations' to SITES list - Dockerfile: EXPOSE 40000-40014 -> 40000-40015 Seed DB (instance_seed/phet_simulations.db, 143KB) packs into a 17KB phet_simulations.tar.gz via scripts/extract_assets.sh and ships separately via the Hugging Face dataset. .assets-revision will need a bump after the HF PR merges. tasks.jsonl: 43 benchmark prompts covering catalog browse, subject filters, simulation detail extraction, search, translations, teacher activities, and the account save flow. --- Dockerfile | 4 +- control_server.py | 1 + sites/phet_simulations/_health.py | 11 + sites/phet_simulations/app.py | 1263 +++++++++++++++++ sites/phet_simulations/static/css/style.css | 433 ++++++ sites/phet_simulations/static/js/main.js | 59 + sites/phet_simulations/tasks.jsonl | 43 + sites/phet_simulations/templates/404.html | 14 + .../phet_simulations/templates/_sim_card.html | 18 + sites/phet_simulations/templates/about.html | 38 + .../templates/accessibility.html | 38 + sites/phet_simulations/templates/account.html | 36 + .../templates/activities.html | 45 + .../templates/activity_detail.html | 44 + sites/phet_simulations/templates/base.html | 100 ++ .../phet_simulations/templates/category.html | 19 + sites/phet_simulations/templates/index.html | 53 + sites/phet_simulations/templates/login.html | 25 + .../phet_simulations/templates/register.html | 37 + sites/phet_simulations/templates/search.html | 25 + .../templates/simulation_detail.html | 122 ++ .../templates/simulations.html | 68 + .../phet_simulations/templates/teachers.html | 32 + .../templates/translation_detail.html | 25 + .../templates/translations.html | 21 + websyn_start.sh | 2 +- 26 files changed, 2573 insertions(+), 3 deletions(-) create mode 100644 sites/phet_simulations/_health.py create mode 100644 sites/phet_simulations/app.py create mode 100644 sites/phet_simulations/static/css/style.css create mode 100644 sites/phet_simulations/static/js/main.js create mode 100644 sites/phet_simulations/tasks.jsonl create mode 100644 sites/phet_simulations/templates/404.html create mode 100644 sites/phet_simulations/templates/_sim_card.html create mode 100644 sites/phet_simulations/templates/about.html create mode 100644 sites/phet_simulations/templates/accessibility.html create mode 100644 sites/phet_simulations/templates/account.html create mode 100644 sites/phet_simulations/templates/activities.html create mode 100644 sites/phet_simulations/templates/activity_detail.html create mode 100644 sites/phet_simulations/templates/base.html create mode 100644 sites/phet_simulations/templates/category.html create mode 100644 sites/phet_simulations/templates/index.html create mode 100644 sites/phet_simulations/templates/login.html create mode 100644 sites/phet_simulations/templates/register.html create mode 100644 sites/phet_simulations/templates/search.html create mode 100644 sites/phet_simulations/templates/simulation_detail.html create mode 100644 sites/phet_simulations/templates/simulations.html create mode 100644 sites/phet_simulations/templates/teachers.html create mode 100644 sites/phet_simulations/templates/translation_detail.html create mode 100644 sites/phet_simulations/templates/translations.html diff --git a/Dockerfile b/Dockerfile index c5a2cbab7..61cbfd0e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 29 Flask mirror sites + control plane on :8101. +# 30 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -92,6 +92,6 @@ os.makedirs('instance_seed', exist_ok=True); \ shutil.copy2('instance/rotten_tomatoes.db', 'instance_seed/rotten_tomatoes.db'); \ print('Rotten Tomatoes seed DB generated at build time.')" && rm -rf /opt/WebSyn/rotten_tomatoes/instance -EXPOSE 8101 40000-40028 +EXPOSE 8101 40000-40029 CMD ["/opt/websyn_start.sh"] diff --git a/control_server.py b/control_server.py index 9e2ca7164..717899603 100644 --- a/control_server.py +++ b/control_server.py @@ -31,6 +31,7 @@ 'osu', 'rotten_tomatoes', 'compass', 'walmart_careers', 'fedex', 'webmd_doctor', 'healthline', 'kaggle', 'nvidia', + 'phet_simulations', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/sites/phet_simulations/_health.py b/sites/phet_simulations/_health.py new file mode 100644 index 000000000..df949a48a --- /dev/null +++ b/sites/phet_simulations/_health.py @@ -0,0 +1,11 @@ +"""Simple health probe for the PhET Simulations mirror. + +Returned by the /_health endpoint. The control plane only inspects HTTP +status, so any 2xx response with a JSON body is sufficient — the payload +shape mirrors the scaffold default and is also surfaced verbatim to +human reviewers. +""" + + +def health(): + return {"ok": True, "site": "phet_simulations"} diff --git a/sites/phet_simulations/app.py b/sites/phet_simulations/app.py new file mode 100644 index 000000000..0d8655086 --- /dev/null +++ b/sites/phet_simulations/app.py @@ -0,0 +1,1263 @@ +"""PhET Interactive Simulations mirror. + +Mirrors the structure of https://phet.colorado.edu/ for WebHarbor agent +evaluation: a Flask + SQLite app that serves a deterministic snapshot of +the PhET simulation catalog (browse, filter, search, translations, +teacher activities, account-gated saves). +""" +import json +import os +import re +from datetime import date, datetime +from pathlib import Path + +from flask import ( + Flask, abort, flash, jsonify, redirect, render_template, request, + session, url_for, +) +from flask_bcrypt import Bcrypt +from flask_login import ( + LoginManager, UserMixin, current_user, login_required, login_user, + logout_user, +) +from flask_sqlalchemy import SQLAlchemy +from flask_wtf import CSRFProtect +from sqlalchemy import or_ + +from _health import health as _health_payload + + +BASE_DIR = Path(__file__).parent +DB_DIR = BASE_DIR / "instance" +DB_DIR.mkdir(exist_ok=True) +DB_PATH = DB_DIR / "phet_simulations.db" + +app = Flask(__name__) +app.config["SECRET_KEY"] = "phet-simulations-dev-secret-key-do-not-use-in-prod" +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +app.config["WTF_CSRF_TIME_LIMIT"] = None +app.config["TEMPLATES_AUTO_RELOAD"] = True +app.config["JSON_SORT_KEYS"] = False + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message = "Please log in to access this page." +csrf = CSRFProtect(app) + + +# --------------------------------------------------------------------------- +# Models +# --------------------------------------------------------------------------- + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False, index=True) + name = db.Column(db.String(80), nullable=False) + password_hash = db.Column(db.String(200), nullable=False) + role = db.Column(db.String(20), default="teacher") + institution = db.Column(db.String(200)) + country = db.Column(db.String(80)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + saved = db.relationship( + "SavedSimulation", backref="user", lazy="dynamic", + cascade="all, delete-orphan", + ) + + +class Subject(db.Model): + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(40), unique=True, nullable=False, index=True) + name = db.Column(db.String(80), nullable=False) + icon = db.Column(db.String(40)) + color = db.Column(db.String(20)) + description = db.Column(db.Text) + + +class GradeLevel(db.Model): + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(40), unique=True, nullable=False, index=True) + name = db.Column(db.String(80), nullable=False) + age_range = db.Column(db.String(40)) + sort_order = db.Column(db.Integer, default=0) + + +class Language(db.Model): + id = db.Column(db.Integer, primary_key=True) + code = db.Column(db.String(10), unique=True, nullable=False, index=True) + name = db.Column(db.String(80), nullable=False) + native_name = db.Column(db.String(80), nullable=False) + sim_count = db.Column(db.Integer, default=0) + is_rtl = db.Column(db.Boolean, default=False) + + +class Simulation(db.Model): + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(80), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + short_description = db.Column(db.String(300), nullable=False) + overview = db.Column(db.Text, nullable=False) + subjects_json = db.Column(db.Text, default="[]") + grades_json = db.Column(db.Text, default="[]") + topics_json = db.Column(db.Text, default="[]") + languages_json = db.Column(db.Text, default='["en"]') + version = db.Column(db.String(20), default="1.0.0") + is_html5 = db.Column(db.Boolean, default=True) + is_featured = db.Column(db.Boolean, default=False) + is_new = db.Column(db.Boolean, default=False) + thumbnail = db.Column(db.String(120)) + runtime_minutes = db.Column(db.Integer, default=20) + release_date = db.Column(db.Date) + download_count = db.Column(db.Integer, default=0) + play_count = db.Column(db.Integer, default=0) + activities = db.relationship( + "Activity", backref="simulation", lazy="dynamic", + cascade="all, delete-orphan", + ) + saved_by = db.relationship( + "SavedSimulation", backref="simulation", lazy="dynamic", + cascade="all, delete-orphan", + ) + + def subjects(self): + return json.loads(self.subjects_json or "[]") + + def grades(self): + return json.loads(self.grades_json or "[]") + + def topics(self): + return json.loads(self.topics_json or "[]") + + def languages(self): + return json.loads(self.languages_json or '["en"]') + + +class Activity(db.Model): + id = db.Column(db.Integer, primary_key=True) + sim_id = db.Column( + db.Integer, db.ForeignKey("simulation.id"), nullable=False, index=True, + ) + title = db.Column(db.String(200), nullable=False) + author = db.Column(db.String(120), nullable=False) + grade_level = db.Column(db.String(40)) + duration_min = db.Column(db.Integer) + description = db.Column(db.Text, nullable=False) + file_type = db.Column(db.String(20), default="PDF") + download_count = db.Column(db.Integer, default=0) + published_date = db.Column(db.Date) + + +class SavedSimulation(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, db.ForeignKey("user.id"), nullable=False, index=True, + ) + sim_id = db.Column( + db.Integer, db.ForeignKey("simulation.id"), nullable=False, index=True, + ) + notes = db.Column(db.Text) + saved_at = db.Column(db.DateTime, default=datetime.utcnow) + __table_args__ = (db.UniqueConstraint("user_id", "sim_id"),) + + +@login_manager.user_loader +def load_user(uid): + return User.query.get(int(uid)) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _subject_map(): + return {s.slug: s for s in Subject.query.all()} + + +def _grade_map(): + return {g.slug: g for g in GradeLevel.query.order_by(GradeLevel.sort_order).all()} + + +def _language_map(): + return {l.code: l for l in Language.query.order_by(Language.name).all()} + + +def _saved_sim_ids(): + if not current_user.is_authenticated: + return set() + return {s.sim_id for s in current_user.saved.all()} + + +@app.context_processor +def inject_globals(): + return { + "site_title": "PhET Interactive Simulations", + "site_tagline": "Free online math and science simulations", + "current_year": datetime.utcnow().year, + "primary_subjects": Subject.query.order_by(Subject.name).all(), + "grade_levels": GradeLevel.query.order_by(GradeLevel.sort_order).all(), + "saved_sim_ids": _saved_sim_ids(), + } + + +# --------------------------------------------------------------------------- +# Routes — public +# --------------------------------------------------------------------------- + +@app.route("/_health") +def health(): + return jsonify(_health_payload()) + + +@app.route("/") +def index(): + featured = ( + Simulation.query.filter_by(is_featured=True) + .order_by(Simulation.title) + .limit(8) + .all() + ) + new_sims = ( + Simulation.query.filter_by(is_new=True) + .order_by(Simulation.release_date.desc()) + .limit(6) + .all() + ) + most_played = ( + Simulation.query.order_by(Simulation.play_count.desc()) + .limit(6) + .all() + ) + total = Simulation.query.count() + total_languages = Language.query.count() + return render_template( + "index.html", + featured=featured, + new_sims=new_sims, + most_played=most_played, + total_simulations=total, + total_languages=total_languages, + ) + + +@app.route("/simulations") +def simulations(): + subject = request.args.get("subject", "").strip() + grade = request.args.get("grade", "").strip() + language = request.args.get("language", "").strip() + sort = request.args.get("sort", "title") + page = max(int(request.args.get("page", 1)), 1) + per_page = 12 + + query = Simulation.query + if subject: + query = query.filter(Simulation.subjects_json.like(f'%"{subject}"%')) + if grade: + query = query.filter(Simulation.grades_json.like(f'%"{grade}"%')) + if language: + query = query.filter(Simulation.languages_json.like(f'%"{language}"%')) + + if sort == "newest": + query = query.order_by(Simulation.release_date.desc()) + elif sort == "popular": + query = query.order_by(Simulation.play_count.desc()) + else: + query = query.order_by(Simulation.title) + + total = query.count() + sims = query.offset((page - 1) * per_page).limit(per_page).all() + total_pages = max((total + per_page - 1) // per_page, 1) + + return render_template( + "simulations.html", + sims=sims, + total=total, + page=page, + total_pages=total_pages, + subject=subject, + grade=grade, + language=language, + sort=sort, + languages=Language.query.order_by(Language.name).all(), + ) + + +@app.route("/simulations/category/") +def simulations_by_subject(slug): + subject = Subject.query.filter_by(slug=slug).first_or_404() + sims = ( + Simulation.query.filter(Simulation.subjects_json.like(f'%"{slug}"%')) + .order_by(Simulation.title) + .all() + ) + return render_template( + "category.html", subject=subject, sims=sims, + ) + + +@app.route("/simulation/") +def simulation_detail(slug): + sim = Simulation.query.filter_by(slug=slug).first_or_404() + sim.play_count = (sim.play_count or 0) + 1 + db.session.commit() + + subjects_full = [ + s for s in Subject.query.filter(Subject.slug.in_(sim.subjects())).all() + ] + grades_full = [ + g for g in GradeLevel.query.filter(GradeLevel.slug.in_(sim.grades())).all() + ] + langs_full = [ + l for l in Language.query.filter(Language.code.in_(sim.languages())).all() + ] + + related = ( + Simulation.query.filter(Simulation.id != sim.id) + .filter( + or_(*[ + Simulation.subjects_json.like(f'%"{s}"%') for s in sim.subjects() + ]) + ) + .order_by(Simulation.title) + .limit(6) + .all() + ) + activities = sim.activities.order_by(Activity.published_date.desc()).all() + + is_saved = ( + current_user.is_authenticated + and SavedSimulation.query.filter_by( + user_id=current_user.id, sim_id=sim.id, + ).first() + is not None + ) + + return render_template( + "simulation_detail.html", + sim=sim, + subjects=subjects_full, + grades=grades_full, + languages=langs_full, + related=related, + activities=activities, + is_saved=is_saved, + ) + + +@app.route("/search") +def search(): + q = request.args.get("q", "").strip() + sims = [] + if q: + pattern = f"%{q}%" + sims = ( + Simulation.query.filter( + or_( + Simulation.title.ilike(pattern), + Simulation.short_description.ilike(pattern), + Simulation.topics_json.ilike(pattern), + ) + ) + .order_by(Simulation.title) + .all() + ) + return render_template("search.html", query=q, sims=sims, total=len(sims)) + + +@app.route("/translations") +def translations(): + langs = ( + Language.query.order_by(Language.sim_count.desc(), Language.name) + .all() + ) + return render_template("translations.html", languages=langs) + + +@app.route("/translations/") +def translation_detail(code): + lang = Language.query.filter_by(code=code).first_or_404() + sims = ( + Simulation.query.filter(Simulation.languages_json.like(f'%"{code}"%')) + .order_by(Simulation.title) + .all() + ) + return render_template( + "translation_detail.html", language=lang, sims=sims, + ) + + +@app.route("/teachers") +def teachers(): + featured = ( + Activity.query.order_by(Activity.download_count.desc()) + .limit(6) + .all() + ) + return render_template("teachers.html", featured_activities=featured) + + +@app.route("/teachers/activities") +def activities(): + grade = request.args.get("grade", "") + query = Activity.query + if grade: + query = query.filter_by(grade_level=grade) + items = query.order_by(Activity.published_date.desc()).all() + return render_template("activities.html", activities=items, grade=grade) + + +@app.route("/teachers/activity/") +def activity_detail(activity_id): + activity = Activity.query.get_or_404(activity_id) + activity.download_count = (activity.download_count or 0) + 1 + db.session.commit() + return render_template("activity_detail.html", activity=activity) + + +@app.route("/about") +def about(): + stats = { + "simulations": Simulation.query.count(), + "languages": Language.query.count(), + "subjects": Subject.query.count(), + "activities": Activity.query.count(), + } + return render_template("about.html", stats=stats) + + +@app.route("/accessibility") +def accessibility(): + return render_template("accessibility.html") + + +# --------------------------------------------------------------------------- +# Routes — auth +# --------------------------------------------------------------------------- + +EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if current_user.is_authenticated: + return redirect(url_for("account")) + if request.method == "POST": + email = request.form.get("email", "").strip().lower() + name = request.form.get("name", "").strip() + password = request.form.get("password", "") + institution = request.form.get("institution", "").strip() + country = request.form.get("country", "").strip() + + if not EMAIL_RE.match(email): + flash("Please enter a valid email address.", "error") + elif len(name) < 2: + flash("Please enter your full name.", "error") + elif len(password) < 8: + flash("Password must be at least 8 characters.", "error") + elif User.query.filter_by(email=email).first(): + flash("An account with that email already exists.", "error") + else: + user = User( + email=email, + name=name, + password_hash=bcrypt.generate_password_hash(password).decode(), + institution=institution, + country=country, + ) + db.session.add(user) + db.session.commit() + login_user(user) + flash("Welcome to PhET! Your account is ready.", "success") + return redirect(url_for("account")) + return render_template("register.html") + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("account")) + if request.method == "POST": + email = request.form.get("email", "").strip().lower() + password = request.form.get("password", "") + user = User.query.filter_by(email=email).first() + if user and bcrypt.check_password_hash(user.password_hash, password): + login_user(user) + flash(f"Welcome back, {user.name}!", "success") + return redirect(request.args.get("next") or url_for("account")) + flash("Invalid email or password.", "error") + return render_template("login.html") + + +@app.route("/logout") +@login_required +def logout(): + logout_user() + flash("You have been signed out.", "success") + return redirect(url_for("index")) + + +@app.route("/account") +@login_required +def account(): + saved_rows = ( + SavedSimulation.query.filter_by(user_id=current_user.id) + .order_by(SavedSimulation.saved_at.desc()) + .all() + ) + return render_template("account.html", saved_rows=saved_rows) + + +@app.route("/api/save-sim", methods=["POST"]) +@login_required +def api_save_sim(): + data = request.get_json(silent=True) or request.form + sim_id = data.get("sim_id") + notes = (data.get("notes") or "").strip() + if not sim_id: + return jsonify({"ok": False, "error": "missing sim_id"}), 400 + sim = Simulation.query.get(int(sim_id)) + if not sim: + return jsonify({"ok": False, "error": "unknown simulation"}), 404 + existing = SavedSimulation.query.filter_by( + user_id=current_user.id, sim_id=sim.id, + ).first() + if existing: + existing.notes = notes or existing.notes + else: + db.session.add( + SavedSimulation( + user_id=current_user.id, sim_id=sim.id, notes=notes, + ) + ) + db.session.commit() + return jsonify({"ok": True, "saved": True, "sim_id": sim.id}) + + +@app.route("/api/unsave-sim", methods=["POST"]) +@login_required +def api_unsave_sim(): + data = request.get_json(silent=True) or request.form + sim_id = data.get("sim_id") + if not sim_id: + return jsonify({"ok": False, "error": "missing sim_id"}), 400 + row = SavedSimulation.query.filter_by( + user_id=current_user.id, sim_id=int(sim_id), + ).first() + if row: + db.session.delete(row) + db.session.commit() + return jsonify({"ok": True, "saved": False, "sim_id": int(sim_id)}) + + +# --------------------------------------------------------------------------- +# Error handlers +# --------------------------------------------------------------------------- + +@app.errorhandler(404) +def not_found(_): + return render_template("404.html"), 404 + + +# --------------------------------------------------------------------------- +# Seed data +# --------------------------------------------------------------------------- + +SUBJECTS_SEED = [ + ("physics", "Physics", "atom", "#0079bf", + "Explore motion, forces, energy, waves, and electromagnetism."), + ("chemistry", "Chemistry", "flask", "#f5862e", + "Build molecules, balance equations, and probe matter."), + ("math", "Math", "function", "#6cba5c", + "Visualize numbers, fractions, functions, and geometry."), + ("biology", "Biology", "leaf", "#7a3e9d", + "Study cells, genetics, evolution, and the human body."), + ("earth-science", "Earth Science", "globe", "#c0392b", + "Investigate Earth's systems, climate, and the solar system."), +] + +GRADES_SEED = [ + ("elementary", "Elementary School", "Ages 5-10", 1), + ("middle", "Middle School", "Ages 11-13", 2), + ("high", "High School", "Ages 14-18", 3), + ("university", "University", "Ages 18+", 4), +] + +LANGUAGES_SEED = [ + ("en", "English", "English", False), + ("es", "Spanish", "Espanol", False), + ("zh-cn", "Chinese (Simplified)", "Zhongwen", False), + ("zh-tw", "Chinese (Traditional)", "Zhongwen", False), + ("fr", "French", "Francais", False), + ("de", "German", "Deutsch", False), + ("pt-br", "Portuguese (Brazilian)", "Portugues", False), + ("ru", "Russian", "Russkiy", False), + ("ar", "Arabic", "Al-Arabiyyah", True), + ("ja", "Japanese", "Nihongo", False), + ("ko", "Korean", "Hangugeo", False), + ("it", "Italian", "Italiano", False), + ("nl", "Dutch", "Nederlands", False), + ("pl", "Polish", "Polski", False), + ("sv", "Swedish", "Svenska", False), + ("tr", "Turkish", "Turkce", False), + ("vi", "Vietnamese", "Tieng Viet", False), + ("hi", "Hindi", "Hindi", False), + ("he", "Hebrew", "Ivrit", True), + ("el", "Greek", "Ellinika", False), + ("cs", "Czech", "Cestina", False), + ("hu", "Hungarian", "Magyar", False), + ("fi", "Finnish", "Suomi", False), + ("da", "Danish", "Dansk", False), + ("ro", "Romanian", "Romana", False), + ("uk", "Ukrainian", "Ukrayinska", False), + ("fa", "Persian", "Farsi", True), + ("id", "Indonesian", "Bahasa Indonesia", False), +] + + +def _slug(title): + return re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") + + +SIMULATIONS_SEED = [ + # (title, subjects, grades, topics, languages_extra, featured, is_new, runtime, year, month, day, play_count) + ("Gravity and Orbits", ["physics", "earth-science"], ["middle", "high"], + ["gravity", "orbits", "circular motion", "satellites"], + ["es", "fr", "de", "zh-cn", "ja", "ar", "pt-br", "ru"], + True, False, 25, 2024, 3, 12, 482103), + ("Forces and Motion: Basics", ["physics"], ["elementary", "middle", "high"], + ["newton's laws", "friction", "acceleration"], + ["es", "fr", "de", "zh-cn", "pt-br", "ru", "ja", "ko", "it", "nl"], + True, False, 20, 2023, 9, 5, 1204567), + ("Energy Skate Park", ["physics"], ["middle", "high", "university"], + ["kinetic energy", "potential energy", "conservation"], + ["es", "fr", "de", "pt-br", "ru", "zh-cn", "ja"], + True, False, 30, 2024, 1, 18, 768922), + ("Wave Interference", ["physics"], ["high", "university"], + ["waves", "interference", "diffraction", "light"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 11, 2, 312045), + ("Faraday's Law", ["physics"], ["high", "university"], + ["electromagnetism", "induction", "magnetic flux"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 20, 2023, 7, 14, 198320), + ("Charges and Fields", ["physics"], ["high", "university"], + ["electrostatics", "electric field", "voltage"], + ["es", "fr", "de", "pt-br"], + False, False, 25, 2023, 6, 22, 234110), + ("Pendulum Lab", ["physics", "math"], ["middle", "high"], + ["pendulum", "period", "gravity", "harmonic motion"], + ["es", "fr", "de", "zh-cn", "ja", "ko"], + True, False, 20, 2024, 2, 9, 521987), + ("Projectile Motion", ["physics"], ["high", "university"], + ["kinematics", "trajectory", "air resistance"], + ["es", "fr", "de", "zh-cn", "pt-br", "ja"], + False, False, 25, 2023, 10, 28, 645321), + ("Circuit Construction Kit: DC", ["physics"], ["middle", "high", "university"], + ["circuits", "ohm's law", "resistance", "current"], + ["es", "fr", "de", "pt-br", "zh-cn", "ja", "ko", "ru", "it"], + True, False, 30, 2024, 4, 1, 892341), + ("Bending Light", ["physics"], ["middle", "high"], + ["refraction", "snell's law", "optics"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 8, 15, 167823), + ("Color Vision", ["physics", "biology"], ["elementary", "middle"], + ["light", "color", "vision", "wavelength"], + ["es", "fr", "de", "ja"], + False, False, 15, 2023, 5, 19, 198765), + ("Coulomb's Law", ["physics"], ["high", "university"], + ["electrostatics", "force", "charge"], + ["es", "fr", "de"], + False, True, 20, 2025, 1, 14, 89234), + ("Hooke's Law", ["physics"], ["middle", "high"], + ["springs", "elasticity", "force"], + ["es", "fr", "de", "zh-cn"], + False, False, 15, 2023, 4, 7, 154322), + ("Magnet and Compass", ["physics", "earth-science"], ["elementary", "middle"], + ["magnetism", "compass", "field lines"], + ["es", "fr", "de", "zh-cn", "ja", "ar"], + False, False, 15, 2023, 3, 21, 232109), + ("Resistance in a Wire", ["physics"], ["high"], + ["resistance", "resistivity", "circuits"], + ["es", "fr", "de"], + False, False, 15, 2023, 2, 4, 87654), + ("Quantum Wave Interference", ["physics"], ["university"], + ["quantum mechanics", "wave-particle duality"], + ["es", "fr", "de"], + False, True, 35, 2025, 2, 28, 45120), + + ("Build a Molecule", ["chemistry"], ["middle", "high"], + ["molecules", "atoms", "bonding"], + ["es", "fr", "de", "zh-cn", "ja", "pt-br", "ru"], + True, False, 25, 2024, 1, 22, 678901), + ("Balancing Chemical Equations", ["chemistry"], ["middle", "high"], + ["stoichiometry", "equations", "reactions"], + ["es", "fr", "de", "zh-cn", "pt-br", "ko"], + True, False, 20, 2023, 12, 6, 543210), + ("States of Matter", ["chemistry", "physics"], ["elementary", "middle", "high"], + ["solid", "liquid", "gas", "phase change"], + ["es", "fr", "de", "zh-cn", "ja", "ar", "pt-br", "ru", "it"], + True, False, 25, 2024, 2, 14, 891234), + ("Concentration", ["chemistry"], ["high", "university"], + ["solutions", "molarity", "dilution"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 11, 9, 234567), + ("pH Scale", ["chemistry"], ["middle", "high"], + ["acids", "bases", "ph", "hydrogen ions"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 15, 2023, 9, 17, 312456), + ("Beer's Law Lab", ["chemistry"], ["high", "university"], + ["absorbance", "spectroscopy", "concentration"], + ["es", "fr", "de"], + False, False, 25, 2023, 8, 3, 167890), + ("Acid-Base Solutions", ["chemistry"], ["high", "university"], + ["acids", "bases", "equilibrium"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 7, 25, 198432), + ("Reactions and Rates", ["chemistry"], ["high", "university"], + ["kinetics", "reactions", "activation energy"], + ["es", "fr", "de"], + False, False, 30, 2023, 6, 11, 145678), + ("Molecule Polarity", ["chemistry"], ["high"], + ["polarity", "electronegativity", "dipole"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 5, 8, 123456), + ("Isotopes and Atomic Mass", ["chemistry"], ["high", "university"], + ["isotopes", "atomic mass", "elements"], + ["es", "fr", "de"], + False, False, 20, 2023, 4, 16, 98765), + ("Build an Atom", ["chemistry", "physics"], ["middle", "high"], + ["atoms", "protons", "neutrons", "electrons"], + ["es", "fr", "de", "zh-cn", "ja", "ko", "ar", "ru", "pt-br"], + True, False, 20, 2024, 3, 7, 712345), + ("Salts and Solubility", ["chemistry"], ["high"], + ["solubility", "salts", "saturation"], + ["es", "fr", "de"], + False, True, 25, 2025, 3, 4, 56789), + + ("Graphing Lines", ["math"], ["middle", "high"], + ["linear equations", "slope", "intercept"], + ["es", "fr", "de", "zh-cn", "ja", "ko", "ar"], + True, False, 20, 2024, 1, 11, 567890), + ("Area Builder", ["math"], ["elementary", "middle"], + ["area", "perimeter", "shapes"], + ["es", "fr", "de", "zh-cn", "ja", "pt-br"], + False, False, 15, 2023, 10, 14, 234567), + ("Fractions: Intro", ["math"], ["elementary", "middle"], + ["fractions", "numerators", "denominators"], + ["es", "fr", "de", "zh-cn", "ja", "ar", "pt-br", "ru"], + True, False, 15, 2024, 2, 19, 689012), + ("Function Builder", ["math"], ["middle", "high"], + ["functions", "input output", "composition"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 9, 21, 178901), + ("Plinko Probability", ["math"], ["middle", "high", "university"], + ["probability", "distributions", "statistics"], + ["es", "fr", "de"], + False, False, 20, 2023, 8, 6, 145678), + ("Trig Tour", ["math"], ["high", "university"], + ["trigonometry", "sine", "cosine", "unit circle"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 7, 30, 123890), + ("Vector Addition", ["math", "physics"], ["high", "university"], + ["vectors", "components", "magnitude"], + ["es", "fr", "de"], + False, False, 20, 2023, 6, 8, 156789), + ("Calculus Grapher", ["math"], ["high", "university"], + ["derivatives", "integrals", "calculus"], + ["es", "fr", "de"], + False, True, 30, 2025, 1, 22, 67890), + ("Equality Explorer", ["math"], ["elementary", "middle"], + ["equations", "balance", "variables"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 5, 12, 134567), + ("Number Line: Integers", ["math"], ["elementary", "middle"], + ["integers", "negative numbers", "number line"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 15, 2023, 4, 19, 98765), + ("Make a Ten", ["math"], ["elementary"], + ["addition", "place value", "counting"], + ["es", "fr", "zh-cn", "ja"], + False, False, 10, 2023, 3, 25, 78901), + ("Estimation", ["math"], ["elementary", "middle"], + ["estimation", "measurement", "comparison"], + ["es", "fr", "de"], + False, False, 15, 2023, 2, 16, 65432), + + ("Natural Selection", ["biology"], ["middle", "high", "university"], + ["evolution", "adaptation", "selection"], + ["es", "fr", "de", "zh-cn", "ja", "pt-br"], + True, False, 30, 2024, 1, 27, 412567), + ("Gene Expression Essentials", ["biology"], ["high", "university"], + ["dna", "transcription", "translation", "proteins"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 11, 23, 234890), + ("Neuron", ["biology"], ["high", "university"], + ["neurons", "action potential", "ion channels"], + ["es", "fr", "de"], + False, False, 25, 2023, 10, 5, 187654), + ("Membrane Channels", ["biology"], ["high", "university"], + ["membranes", "diffusion", "transport"], + ["es", "fr", "de"], + False, False, 20, 2023, 9, 12, 145623), + ("Stretching DNA", ["biology", "physics"], ["high", "university"], + ["dna", "forces", "molecular biology"], + ["es", "fr", "de"], + False, False, 25, 2023, 8, 28, 112345), + ("Eating and Exercise", ["biology"], ["middle", "high"], + ["nutrition", "metabolism", "calories"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 7, 17, 167890), + + ("Plate Tectonics", ["earth-science"], ["middle", "high"], + ["plates", "continents", "earthquakes"], + ["es", "fr", "de", "zh-cn", "ja"], + True, False, 25, 2024, 2, 5, 389012), + ("Greenhouse Effect", ["earth-science", "physics"], ["middle", "high"], + ["climate", "atmosphere", "radiation"], + ["es", "fr", "de", "zh-cn", "pt-br"], + True, False, 25, 2024, 3, 18, 456789), + ("Glaciers", ["earth-science"], ["middle", "high"], + ["glaciers", "climate", "ice"], + ["es", "fr", "de"], + False, False, 20, 2023, 11, 14, 123456), + ("Density", ["earth-science", "physics"], ["elementary", "middle", "high"], + ["density", "mass", "volume", "buoyancy"], + ["es", "fr", "de", "zh-cn", "ja", "ar"], + True, False, 20, 2024, 1, 30, 567823), + ("My Solar System", ["earth-science", "physics"], ["middle", "high", "university"], + ["gravity", "orbits", "solar system"], + ["es", "fr", "de", "zh-cn", "ja", "ar", "pt-br"], + True, False, 30, 2024, 4, 11, 678901), + ("Radioactive Dating Game", ["earth-science", "physics"], ["high"], + ["radioactivity", "half-life", "dating"], + ["es", "fr", "de"], + False, False, 25, 2023, 6, 23, 134567), + ("Lunar Lander", ["earth-science", "physics"], ["middle", "high"], + ["gravity", "thrust", "motion"], + ["es", "fr", "de", "ja"], + False, True, 20, 2025, 2, 17, 78901), + + # Additional biology sims + ("Mendelian Genetics", ["biology"], ["middle", "high", "university"], + ["genetics", "heredity", "alleles", "punnett squares"], + ["es", "fr", "de", "zh-cn", "ja", "pt-br"], + True, False, 30, 2024, 2, 22, 287654), + ("DNA Replication", ["biology"], ["high", "university"], + ["dna", "replication", "polymerase"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 12, 17, 178923), + ("Photosynthesis", ["biology"], ["elementary", "middle", "high"], + ["photosynthesis", "chlorophyll", "plants", "light"], + ["es", "fr", "de", "zh-cn", "ja", "ko", "pt-br"], + True, False, 25, 2024, 3, 9, 421890), + ("Predator-Prey Dynamics", ["biology"], ["middle", "high"], + ["ecology", "population", "food chain"], + ["es", "fr", "de", "zh-cn"], + False, False, 30, 2023, 11, 26, 198765), + ("Cellular Respiration", ["biology", "chemistry"], ["high", "university"], + ["respiration", "atp", "mitochondria", "energy"], + ["es", "fr", "de"], + False, False, 25, 2023, 10, 13, 156789), + ("Punnett Squares", ["biology"], ["middle", "high"], + ["genetics", "heredity", "punnett squares"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 20, 2023, 9, 8, 234156), + ("Population Dynamics", ["biology", "math"], ["high", "university"], + ["population", "exponential growth", "carrying capacity"], + ["es", "fr", "de"], + False, True, 30, 2025, 1, 8, 67890), + ("Cell Diffusion", ["biology"], ["middle", "high"], + ["diffusion", "membranes", "concentration"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 8, 19, 132465), + ("Enzyme Kinetics", ["biology", "chemistry"], ["high", "university"], + ["enzymes", "catalysis", "kinetics"], + ["es", "fr", "de"], + False, False, 25, 2023, 7, 11, 98432), + ("Food Web Builder", ["biology"], ["elementary", "middle"], + ["ecology", "food web", "trophic levels"], + ["es", "fr", "de", "zh-cn", "ja", "ar"], + False, False, 25, 2023, 6, 4, 187234), + ("Mitosis and Meiosis", ["biology"], ["high", "university"], + ["cell division", "chromosomes", "mitosis", "meiosis"], + ["es", "fr", "de"], + False, False, 30, 2023, 5, 17, 145678), + ("Blood Pressure Basics", ["biology"], ["middle", "high"], + ["circulation", "heart", "blood pressure"], + ["es", "fr", "de", "zh-cn"], + False, False, 20, 2023, 4, 22, 87654), + ("Lac Operon Regulation", ["biology"], ["university"], + ["gene regulation", "operon", "molecular biology"], + ["es", "fr", "de"], + False, False, 30, 2023, 3, 12, 54321), + ("Bee Hive Activity", ["biology"], ["elementary", "middle"], + ["pollination", "bees", "ecosystems"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 15, 2023, 2, 28, 76543), + + # Additional earth-science sims + ("Seasons", ["earth-science"], ["elementary", "middle"], + ["seasons", "earth tilt", "sun"], + ["es", "fr", "de", "zh-cn", "ja", "ar"], + True, False, 20, 2024, 1, 19, 312456), + ("Water Cycle", ["earth-science"], ["elementary", "middle", "high"], + ["evaporation", "condensation", "precipitation", "water cycle"], + ["es", "fr", "de", "zh-cn", "ja", "ko", "pt-br"], + True, False, 20, 2024, 2, 26, 398765), + ("Volcanic Eruption", ["earth-science"], ["elementary", "middle", "high"], + ["volcanoes", "magma", "lava", "geology"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 25, 2023, 12, 10, 234567), + ("Earthquake Simulator", ["earth-science"], ["middle", "high"], + ["earthquakes", "seismic waves", "magnitude"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 11, 18, 198432), + ("Tides", ["earth-science", "physics"], ["middle", "high"], + ["tides", "gravity", "moon", "ocean"], + ["es", "fr", "de"], + False, False, 20, 2023, 10, 27, 167890), + ("Climate Change Model", ["earth-science"], ["high", "university"], + ["climate", "greenhouse gases", "temperature"], + ["es", "fr", "de", "zh-cn", "pt-br"], + True, True, 35, 2025, 2, 8, 89012), + ("Ozone Layer", ["earth-science", "chemistry"], ["middle", "high"], + ["ozone", "atmosphere", "uv radiation"], + ["es", "fr", "de"], + False, False, 20, 2023, 9, 14, 123456), + ("Solar Wind", ["earth-science", "physics"], ["high", "university"], + ["solar wind", "magnetosphere", "auroras"], + ["es", "fr", "de"], + False, False, 25, 2023, 8, 22, 87432), + ("Rock Cycle", ["earth-science"], ["elementary", "middle", "high"], + ["rocks", "minerals", "igneous", "sedimentary", "metamorphic"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 25, 2023, 7, 6, 156789), + ("Ocean Currents", ["earth-science"], ["middle", "high"], + ["ocean", "currents", "thermohaline"], + ["es", "fr", "de"], + False, False, 25, 2023, 6, 18, 112345), + ("Mineral Hardness", ["earth-science"], ["elementary", "middle"], + ["minerals", "mohs scale", "geology"], + ["es", "fr", "de"], + False, False, 15, 2023, 5, 30, 76543), + ("Mountain Building", ["earth-science"], ["middle", "high"], + ["tectonics", "mountains", "erosion"], + ["es", "fr", "de"], + False, False, 25, 2023, 4, 11, 98765), + + # More elementary-friendly sims + ("Shapes and Patterns", ["math"], ["elementary"], + ["shapes", "patterns", "geometry"], + ["es", "fr", "de", "zh-cn", "ja", "ko"], + False, False, 15, 2023, 3, 18, 134567), + ("Counting Coins", ["math"], ["elementary"], + ["counting", "money", "addition"], + ["es", "fr", "de", "zh-cn"], + False, False, 12, 2023, 2, 23, 87654), + ("Simple Machines", ["physics"], ["elementary", "middle"], + ["levers", "pulleys", "wheels", "force"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 20, 2023, 4, 27, 165432), + ("Magnet Toy", ["physics"], ["elementary"], + ["magnets", "attraction", "repulsion"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 12, 2023, 5, 9, 98432), + ("Weather Watcher", ["earth-science"], ["elementary"], + ["weather", "clouds", "temperature"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 15, 2023, 6, 13, 76543), + ("Plant Growth", ["biology"], ["elementary"], + ["plants", "growth", "seeds"], + ["es", "fr", "de", "zh-cn"], + False, False, 15, 2023, 7, 21, 54321), + ("Animal Classification", ["biology"], ["elementary"], + ["animals", "classification", "vertebrates"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 15, 2023, 8, 9, 87654), + + # More chemistry sims (to clear 20+ threshold) + ("Atomic Interactions", ["chemistry", "physics"], ["high", "university"], + ["atoms", "forces", "lennard-jones"], + ["es", "fr", "de"], + False, False, 20, 2023, 3, 14, 123890), + ("Sugar and Salt Solutions", ["chemistry"], ["middle", "high"], + ["solutions", "dissolving", "concentration"], + ["es", "fr", "de", "zh-cn", "ja", "pt-br"], + False, False, 20, 2023, 11, 6, 198765), + ("Gas Properties", ["chemistry", "physics"], ["high", "university"], + ["gases", "pressure", "temperature", "kinetic theory"], + ["es", "fr", "de", "zh-cn"], + True, False, 25, 2024, 1, 25, 287654), + ("Diffusion in Gases", ["chemistry", "physics"], ["middle", "high"], + ["diffusion", "gases", "concentration"], + ["es", "fr", "de"], + False, False, 20, 2023, 10, 19, 134567), + ("Bonding Explorer", ["chemistry"], ["high"], + ["bonding", "ionic", "covalent", "metallic"], + ["es", "fr", "de", "zh-cn"], + False, False, 25, 2023, 9, 28, 156789), + ("Reaction Quizzer", ["chemistry"], ["high", "university"], + ["reactions", "products", "balancing"], + ["es", "fr", "de"], + False, False, 20, 2023, 8, 11, 87654), + + # More math sims (to clear 20+ threshold) + ("Probability Experiments", ["math"], ["middle", "high"], + ["probability", "experiments", "statistics"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 20, 2023, 11, 21, 167890), + ("Coordinate Plane", ["math"], ["middle", "high"], + ["coordinates", "graphs", "plotting"], + ["es", "fr", "de", "zh-cn"], + False, False, 15, 2023, 9, 30, 145678), + ("Algebra Tiles", ["math"], ["middle", "high"], + ["algebra", "polynomials", "factoring"], + ["es", "fr", "de"], + False, False, 25, 2023, 8, 17, 123456), + ("Percent Word Problems", ["math"], ["middle"], + ["percentages", "ratios", "word problems"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 20, 2023, 7, 9, 98432), + ("Geometric Constructions", ["math"], ["middle", "high"], + ["geometry", "compass", "straightedge"], + ["es", "fr", "de"], + False, False, 25, 2023, 6, 5, 87654), + ("Decimal Models", ["math"], ["elementary", "middle"], + ["decimals", "place value", "comparison"], + ["es", "fr", "de", "zh-cn", "ja"], + False, False, 15, 2023, 5, 14, 134567), +] + + +ACTIVITIES_SEED = [ + ("Forces and Motion: Basics", "Net Force Investigation", + "Dr. Trish Loeblein", "high", 50, + "Students predict, observe, and explain the motion of objects with " + "balanced and unbalanced forces using friction and applied force."), + ("Build an Atom", "Atomic Structure Lab", + "Emily Moore", "middle", 45, + "Build atoms of the first 10 elements, identify subatomic particles, " + "and explore how protons determine the element."), + ("Balancing Chemical Equations", "Coefficient Practice", + "Yuen-Ying Carpenter", "high", 60, + "Practice balancing combustion, synthesis, and decomposition reactions " + "using the conservation of mass."), + ("States of Matter", "Phase Change Inquiry", + "Sam McKagan", "middle", 40, + "Investigate the relationship between temperature, kinetic energy, " + "and phase transitions for water, neon, oxygen, and argon."), + ("Natural Selection", "Evolution of Bunnies", + "Wendy Adams", "high", 55, + "Model how variation, environment, and selection pressure together " + "drive allele frequency change across generations."), + ("Gravity and Orbits", "Modeling the Solar System", + "Noah Finkelstein", "middle", 50, + "Manipulate masses and distances to explore how gravitational force " + "shapes planetary orbits in our solar system."), + ("pH Scale", "Acids and Bases in the Kitchen", + "Kelly Lancaster", "middle", 40, + "Predict, measure, and rank common household solutions by pH and " + "categorize each as acid, base, or neutral."), + ("Plate Tectonics", "Boundary Identification", + "Karina Hensberry", "high", 45, + "Use animations to classify convergent, divergent, and transform " + "boundaries and connect each to real-world geologic features."), + ("Greenhouse Effect", "Climate Modeling Lab", + "Trish Loeblein", "high", 60, + "Model how atmospheric composition affects equilibrium temperature " + "with and without greenhouse gases."), + ("Circuit Construction Kit: DC", "Series and Parallel", + "John De La Cruz", "high", 50, + "Compare current and voltage in series vs parallel arrangements " + "and verify Kirchhoff's laws empirically."), + ("Fractions: Intro", "Equivalent Fractions Game", + "Amanda McGarry", "elementary", 30, + "Use bar models, number lines, and circle models to identify " + "equivalent fractions and develop fluency."), + ("Energy Skate Park", "Conservation of Energy", + "Karina Hensberry", "high", 55, + "Track potential, kinetic, thermal, and total energy as a skater " + "moves through changing terrain."), + ("Density", "Identify the Mystery Block", + "Emily Moore", "middle", 35, + "Use mass and volume measurements to identify the material of " + "unknown solid blocks and explain buoyancy in water."), + ("Graphing Lines", "Slope-Intercept Form", + "Dr. Karina Hensberry", "middle", 40, + "Investigate how m and b transform the line y = mx + b and apply " + "this to real-world rate problems."), +] + + +BENCHMARK_USERS = [ + ("teacher@phet.test", "Ada Lovelace", "phet-teacher-pass", + "teacher", "Cherry Creek High School", "United States"), + ("student@phet.test", "Carl Sagan", "phet-student-pass", + "student", "Ithaca High School", "United States"), + ("research@phet.test", "Marie Curie", "phet-research-pass", + "researcher", "Sorbonne University", "France"), + ("demo@phet.test", "Demo User", "phet-demo-pass", + "teacher", "Demo School", "Canada"), +] + + +def seed_subjects(): + if Subject.query.count() > 0: + return + for slug, name, icon, color, desc in SUBJECTS_SEED: + db.session.add(Subject( + slug=slug, name=name, icon=icon, color=color, description=desc, + )) + db.session.commit() + + +def seed_grades(): + if GradeLevel.query.count() > 0: + return + for slug, name, age_range, sort_order in GRADES_SEED: + db.session.add(GradeLevel( + slug=slug, name=name, age_range=age_range, sort_order=sort_order, + )) + db.session.commit() + + +def seed_languages(): + if Language.query.count() > 0: + return + for code, name, native, rtl in LANGUAGES_SEED: + db.session.add(Language( + code=code, name=name, native_name=native, is_rtl=rtl, sim_count=0, + )) + db.session.commit() + + +def seed_simulations(): + if Simulation.query.count() > 0: + return + for row in SIMULATIONS_SEED: + (title, subjects, grades, topics, extra_langs, + featured, is_new, runtime, year, month, day, plays) = row + slug = _slug(title) + languages = ["en"] + list(extra_langs) + sim = Simulation( + slug=slug, + title=title, + short_description=_make_short_desc(title, topics), + overview=_make_overview(title, topics, subjects), + subjects_json=json.dumps(subjects), + grades_json=json.dumps(grades), + topics_json=json.dumps(topics), + languages_json=json.dumps(languages), + is_html5=True, + is_featured=featured, + is_new=is_new, + thumbnail=f"{slug}.svg", + runtime_minutes=runtime, + release_date=date(year, month, day), + play_count=plays, + download_count=max(plays // 8, 1000), + ) + db.session.add(sim) + db.session.commit() + + # update per-language sim counts + lang_counts = {l.code: 0 for l in Language.query.all()} + for sim in Simulation.query.all(): + for code in sim.languages(): + if code in lang_counts: + lang_counts[code] += 1 + for code, count in lang_counts.items(): + lang = Language.query.filter_by(code=code).first() + if lang: + lang.sim_count = count + db.session.commit() + + +def seed_activities(): + if Activity.query.count() > 0: + return + for sim_title, title, author, grade, duration, desc in ACTIVITIES_SEED: + sim = Simulation.query.filter_by(slug=_slug(sim_title)).first() + if not sim: + continue + db.session.add(Activity( + sim_id=sim.id, + title=title, + author=author, + grade_level=grade, + duration_min=duration, + description=desc, + file_type="PDF", + download_count=max(duration * 137 % 9000, 350), + published_date=date(2024, ((duration % 12) + 1), 15), + )) + db.session.commit() + + +def seed_benchmark_users(): + if User.query.count() > 0: + return + for email, name, password, role, institution, country in BENCHMARK_USERS: + db.session.add(User( + email=email, + name=name, + password_hash=bcrypt.generate_password_hash(password).decode(), + role=role, + institution=institution, + country=country, + )) + db.session.commit() + + # Pre-populate saved sims for the demo teacher so the account page is + # non-empty when an agent inspects it after login. + teacher = User.query.filter_by(email="teacher@phet.test").first() + if teacher: + for slug in ("forces-and-motion-basics", "build-an-atom", + "ph-scale", "natural-selection"): + sim = Simulation.query.filter_by(slug=slug).first() + if sim: + db.session.add(SavedSimulation( + user_id=teacher.id, sim_id=sim.id, + notes=f"Use for {sim.title} unit opener.", + )) + db.session.commit() + + +def _make_short_desc(title, topics): + if not topics: + return f"Interactive simulation: {title}." + topic_str = ", ".join(topics[:3]) + return f"Explore {topic_str} with the interactive {title} simulation." + + +def _make_overview(title, topics, subjects): + subject_names = ", ".join(s.replace("-", " ").title() for s in subjects) + topic_list = ", ".join(topics) if topics else "core concepts" + return ( + f"{title} is an interactive HTML5 simulation in the PhET " + f"{subject_names} collection. Learners investigate {topic_list} " + f"by directly manipulating model parameters and observing " + f"real-time visual feedback. The simulation supports inquiry-" + f"based instruction, formative assessment, and at-home practice, " + f"and is freely available under a Creative Commons license." + ) + + +def seed_all(): + seed_subjects() + seed_grades() + seed_languages() + seed_simulations() + seed_activities() + seed_benchmark_users() + + +with app.app_context(): + db.create_all() + seed_all() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 40015)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/phet_simulations/static/css/style.css b/sites/phet_simulations/static/css/style.css new file mode 100644 index 000000000..b55935f13 --- /dev/null +++ b/sites/phet_simulations/static/css/style.css @@ -0,0 +1,433 @@ +:root { + --c-bg: #ffffff; + --c-bg-alt: #f4f6f8; + --c-border: #d8dde3; + --c-text: #1f2933; + --c-muted: #5b6470; + --c-primary: #0079bf; + --c-primary-dark: #005a8c; + --c-accent: #f5862e; + --c-success: #2e7d32; + --c-error: #c0392b; + --c-green: #6cba5c; + --radius: 6px; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.12); + --max-width: 1180px; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + color: var(--c-text); + background: var(--c-bg); + line-height: 1.5; +} +a { color: var(--c-primary); text-decoration: none; } +a:hover { text-decoration: underline; } +.container { max-width: var(--max-width); margin: 0 auto; padding: 0 1.25rem; } + +/* Topbar */ +.topbar { + background: var(--c-primary-dark); + color: #fff; + padding: 0.75rem 0; + border-bottom: 3px solid var(--c-accent); +} +.topbar-inner { + display: flex; + align-items: center; + gap: 1.25rem; + flex-wrap: wrap; +} +.brand { display: flex; align-items: center; gap: 0.5rem; color: #fff; text-decoration: none; } +.brand-mark { + background: var(--c-accent); + color: #fff; + padding: 0.25rem 0.5rem; + border-radius: var(--radius); + font-weight: 700; + letter-spacing: 0.05em; +} +.brand-text { font-weight: 600; font-size: 1.05rem; } +.primary-nav { display: flex; gap: 1rem; flex: 1 1 auto; } +.primary-nav a { color: #fff; font-size: 0.95rem; } +.primary-nav a:hover { text-decoration: underline; } + +.search-form { display: flex; gap: 0.25rem; } +.search-form input[type=search] { + padding: 0.4rem 0.6rem; + border: 1px solid var(--c-border); + border-radius: var(--radius); + min-width: 200px; +} +.search-form button { + background: var(--c-accent); + color: #fff; + border: 0; + padding: 0.4rem 0.8rem; + border-radius: var(--radius); + cursor: pointer; +} + +.user-menu { display: flex; gap: 0.5rem; align-items: center; } +.user-link { color: #fff; font-size: 0.9rem; } +.user-link.muted { opacity: 0.7; } +.btn-mini { + background: var(--c-accent); + padding: 0.25rem 0.6rem; + border-radius: var(--radius); +} + +/* Flashes */ +.flashes { margin-top: 1rem; } +.flash { + padding: 0.75rem 1rem; + margin-bottom: 0.5rem; + border-radius: var(--radius); + border: 1px solid; +} +.flash-success { background: #e8f5e9; border-color: var(--c-success); color: var(--c-success); } +.flash-error { background: #fdecea; border-color: var(--c-error); color: var(--c-error); } + +/* Layout */ +.main { padding: 2rem 0 3rem; min-height: 60vh; } +.page-title { margin: 0 0 0.5rem; font-size: 1.75rem; } +.page-subtitle { color: var(--c-muted); margin: 0 0 1.5rem; } +.row { margin: 2rem 0; } +.row-title { font-size: 1.3rem; margin: 0 0 1rem; border-bottom: 2px solid var(--c-bg-alt); padding-bottom: 0.35rem; } +.empty-state { background: var(--c-bg-alt); padding: 1rem 1.25rem; border-radius: var(--radius); color: var(--c-muted); } +.breadcrumb { color: var(--c-muted); font-size: 0.9rem; margin-bottom: 0.5rem; } +.breadcrumb a { color: var(--c-primary); } + +.btn { + display: inline-block; + padding: 0.5rem 1rem; + border-radius: var(--radius); + font-weight: 600; + cursor: pointer; + border: 1px solid transparent; + text-decoration: none; + font-size: 0.95rem; +} +.btn-primary { background: var(--c-primary); color: #fff; } +.btn-primary:hover { background: var(--c-primary-dark); text-decoration: none; } +.btn-ghost { background: #fff; color: var(--c-primary); border-color: var(--c-primary); } +.btn-block { display: block; width: 100%; text-align: center; } + +/* Hero */ +.hero { + background: linear-gradient(135deg, #e8f1f8 0%, #d9e9f3 100%); + padding: 2.5rem 2rem; + border-radius: var(--radius); + margin-bottom: 2rem; +} +.hero h1 { margin: 0 0 0.75rem; font-size: 2rem; } +.hero p { margin: 0 0 1rem; max-width: 720px; } +.hero-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; } + +/* Subject tiles */ +.subjects-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin: 1.5rem 0 2.5rem; +} +.subject-tile { + display: flex; + flex-direction: column; + padding: 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--c-border); + border-top: 4px solid var(--tile-color, var(--c-primary)); + background: #fff; + color: var(--c-text); + transition: transform 0.15s, box-shadow 0.15s; +} +.subject-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); text-decoration: none; } +.subject-icon { + width: 40px; height: 40px; + border-radius: 50%; + background: var(--tile-color, var(--c-primary)); + color: #fff; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.6rem; +} +.subject-name { font-weight: 700; font-size: 1.05rem; } +.subject-desc { color: var(--c-muted); font-size: 0.88rem; margin-top: 0.25rem; } + +/* Sim card */ +.sim-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1.25rem; +} +.sim-grid-small { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 1rem; +} +.sim-card { + background: #fff; + border: 1px solid var(--c-border); + border-radius: var(--radius); + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; + transition: box-shadow 0.15s, transform 0.15s; +} +.sim-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-1px); } +.sim-thumb { + background: linear-gradient(135deg, #0079bf 0%, #6cba5c 100%); + aspect-ratio: 16 / 10; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 700; + text-align: center; + padding: 0.5rem; + text-decoration: none; +} +.sim-thumb-label { font-size: 0.95rem; line-height: 1.2; } +.sim-meta { padding: 0.75rem 0.9rem 1rem; flex: 1; display: flex; flex-direction: column; } +.sim-title { margin: 0 0 0.25rem; font-size: 1rem; } +.sim-title a { color: var(--c-text); } +.sim-title a:hover { color: var(--c-primary); text-decoration: underline; } +.sim-desc { color: var(--c-muted); font-size: 0.85rem; margin: 0 0 0.5rem; flex: 1; } +.sim-tags { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.25rem; } +.sim-tag { font-size: 0.72rem; background: var(--c-bg-alt); color: var(--c-muted); padding: 0.15rem 0.4rem; border-radius: 3px; } +.badge { display: inline-block; font-size: 0.7rem; padding: 0.15rem 0.45rem; border-radius: 3px; margin-right: 0.25rem; } +.badge-new { background: var(--c-accent); color: #fff; } +.badge-featured { background: var(--c-green); color: #fff; } +.badge-html5 { background: var(--c-primary); color: #fff; } +.badge-version { background: var(--c-bg-alt); color: var(--c-muted); } + +/* Filter bar */ +.filter-bar { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + align-items: flex-end; + background: var(--c-bg-alt); + padding: 0.85rem 1rem; + border-radius: var(--radius); + margin-bottom: 1.25rem; +} +.filter-bar label { + display: flex; + flex-direction: column; + font-size: 0.8rem; + color: var(--c-muted); + gap: 0.2rem; +} +.filter-bar select { + padding: 0.4rem 0.6rem; + border: 1px solid var(--c-border); + border-radius: var(--radius); + background: #fff; + min-width: 160px; +} +.search-form-wide { + display: flex; + gap: 0.5rem; + margin: 1rem 0 2rem; +} +.search-form-wide input { flex: 1; padding: 0.6rem 0.8rem; border: 1px solid var(--c-border); border-radius: var(--radius); } + +/* Pager */ +.pager { display: flex; gap: 1rem; align-items: center; justify-content: center; margin: 2rem 0; } +.pager-status { color: var(--c-muted); font-size: 0.9rem; } + +/* Sim detail */ +.sim-detail-head { margin-bottom: 1.5rem; } +.sim-detail-head h1 { margin: 0 0 0.5rem; font-size: 2rem; } +.sim-detail-tagline { color: var(--c-muted); font-size: 1.05rem; margin: 0 0 0.75rem; } +.badge-row { list-style: none; padding: 0; margin: 0; display: flex; gap: 0.25rem; flex-wrap: wrap; } +.sim-detail-body { display: grid; grid-template-columns: 2.4fr 1fr; gap: 2rem; } +@media (max-width: 800px) { .sim-detail-body { grid-template-columns: 1fr; } } +.sim-detail-frame { + background: linear-gradient(135deg, #0079bf 0%, #6cba5c 100%); + aspect-ratio: 16 / 10; + border-radius: var(--radius); + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 1rem; + color: #fff; + margin-bottom: 1.5rem; +} +.sim-detail-frame-label { font-size: 1.4rem; font-weight: 700; } +.sim-detail-section { margin: 1.5rem 0; } +.sim-detail-section h2 { font-size: 1.2rem; margin: 0 0 0.5rem; } +.topic-list, .activity-list { padding-left: 1.5rem; } +.activity-list li { margin-bottom: 0.5rem; } +.activity-meta { color: var(--c-muted); font-size: 0.85rem; } +.side-card { + background: var(--c-bg-alt); + border-radius: var(--radius); + padding: 1rem 1.2rem; + margin-bottom: 1rem; +} +.side-card h3 { margin: 0 0 0.5rem; font-size: 0.95rem; } +.side-card ul { margin: 0; padding: 0; list-style: none; } +.side-card li { padding: 0.2rem 0; font-size: 0.9rem; border-bottom: 1px solid #e1e6eb; } +.side-card li:last-child { border: 0; } +.lang-list { max-height: 220px; overflow-y: auto; } +.save-status { color: var(--c-success); font-size: 0.85rem; margin: 0.5rem 0 0; } + +/* Languages */ +.language-grid { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.75rem; +} +.language-card { + background: #fff; + border: 1px solid var(--c-border); + border-radius: var(--radius); +} +.language-card a { + display: flex; + flex-direction: column; + padding: 1rem 1.2rem; + color: var(--c-text); + text-decoration: none; +} +.language-card.rtl a { direction: rtl; text-align: right; } +.language-name { font-weight: 700; font-size: 1rem; } +.language-native { color: var(--c-muted); font-size: 0.88rem; } +.language-count { font-size: 0.8rem; color: var(--c-primary); margin-top: 0.25rem; } + +/* Teachers */ +.teachers-hero { + background: linear-gradient(135deg, #fff4e6 0%, #ffe5cc 100%); + padding: 2rem; + border-radius: var(--radius); + margin-bottom: 2rem; +} +.teachers-hero h1 { margin: 0 0 0.5rem; } +.activity-grid { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} +.activity-card { + background: #fff; + border: 1px solid var(--c-border); + border-radius: var(--radius); + padding: 1.1rem 1.2rem; +} +.activity-card h3 { margin: 0 0 0.4rem; font-size: 1.05rem; } +.activity-author { color: var(--c-muted); font-size: 0.82rem; margin-top: 0.5rem; } +.activity-detail .activity-section { margin: 1.5rem 0; } +.activity-detail h1 { margin: 0 0 0.5rem; } + +/* Category */ +.category-header { + background: var(--c-bg-alt); + border-left: 5px solid var(--tile-color, var(--c-primary)); + padding: 1.5rem; + border-radius: var(--radius); + margin-bottom: 1.5rem; +} +.category-header h1 { margin: 0 0 0.25rem; } +.category-count { color: var(--c-muted); margin: 0.5rem 0 0; } + +/* About */ +.prose { max-width: 720px; } +.prose h2 { margin-top: 2rem; } +.stats-grid { + list-style: none; + padding: 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} +.stat-card { + background: var(--c-bg-alt); + padding: 1.2rem; + border-radius: var(--radius); + text-align: center; +} +.stat-card strong { display: block; font-size: 1.8rem; color: var(--c-primary); } +.stat-card span { color: var(--c-muted); font-size: 0.85rem; } + +/* Account */ +.saved-list { list-style: none; padding: 0; } +.saved-item { + background: #fff; + border: 1px solid var(--c-border); + border-radius: var(--radius); + padding: 1rem 1.25rem; + margin-bottom: 0.75rem; +} +.saved-item h3 { margin: 0 0 0.25rem; font-size: 1.05rem; } +.saved-desc { color: var(--c-muted); margin: 0 0 0.5rem; font-size: 0.9rem; } +.saved-notes { background: #fff8e1; padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.88rem; } +.saved-meta { color: var(--c-muted); font-size: 0.8rem; margin: 0.4rem 0 0; } + +/* Auth */ +.auth-card { + max-width: 420px; + margin: 2rem auto; + background: #fff; + border: 1px solid var(--c-border); + border-radius: var(--radius); + padding: 2rem; + box-shadow: var(--shadow); +} +.auth-card h1 { margin: 0 0 0.5rem; font-size: 1.5rem; } +.auth-sub { color: var(--c-muted); margin: 0 0 1.5rem; font-size: 0.9rem; } +.auth-card label { display: block; margin-bottom: 0.85rem; font-size: 0.88rem; color: var(--c-muted); } +.auth-card input { + display: block; + width: 100%; + margin-top: 0.2rem; + padding: 0.55rem 0.7rem; + border: 1px solid var(--c-border); + border-radius: var(--radius); + font-size: 0.95rem; +} +.auth-foot { color: var(--c-muted); font-size: 0.88rem; margin-top: 1rem; text-align: center; } + +/* Error */ +.error-page { text-align: center; padding: 3rem 1rem; } +.error-page ul { list-style: none; padding: 0; } +.error-page li { display: inline-block; margin: 0 0.5rem; } + +/* Footer */ +.footer { + background: #1f2933; + color: #d4d8de; + padding: 2.5rem 0 1.5rem; + margin-top: 3rem; +} +.footer-inner { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1.5rem; +} +.footer h4 { color: #fff; margin: 0 0 0.6rem; font-size: 0.95rem; } +.footer ul { list-style: none; padding: 0; margin: 0; } +.footer li { margin-bottom: 0.3rem; } +.footer a { color: #d4d8de; font-size: 0.88rem; } +.footer a:hover { color: #fff; } +.footer-fineprint { + border-top: 1px solid #2d3742; + padding-top: 1rem; + margin-top: 1.5rem; + color: #8c95a1; + font-size: 0.82rem; +} diff --git a/sites/phet_simulations/static/js/main.js b/sites/phet_simulations/static/js/main.js new file mode 100644 index 000000000..17df6a4da --- /dev/null +++ b/sites/phet_simulations/static/js/main.js @@ -0,0 +1,59 @@ +(function () { + function getCsrfToken(form) { + var input = form.querySelector('input[name="csrf_token"]'); + return input ? input.value : ''; + } + + document.querySelectorAll('.save-form').forEach(function (form) { + var status = form.querySelector('.save-status'); + form.querySelectorAll('button[data-action]').forEach(function (btn) { + btn.addEventListener('click', function () { + var action = btn.getAttribute('data-action'); + var simId = form.getAttribute('data-sim-id'); + var endpoint = action === 'save' ? '/api/save-sim' : '/api/unsave-sim'; + + fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCsrfToken(form), + }, + body: JSON.stringify({ sim_id: simId }), + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (!data.ok) { + status.textContent = 'Error: ' + (data.error || 'unknown'); + status.style.color = '#c0392b'; + return; + } + if (action === 'save') { + btn.textContent = 'Remove from saved'; + btn.setAttribute('data-action', 'unsave'); + btn.classList.remove('btn-primary'); + btn.classList.add('btn-ghost'); + status.textContent = 'Saved to your account.'; + } else { + btn.textContent = 'Save to my account'; + btn.setAttribute('data-action', 'save'); + btn.classList.remove('btn-ghost'); + btn.classList.add('btn-primary'); + status.textContent = 'Removed from saved.'; + } + status.style.color = '#2e7d32'; + }) + .catch(function () { + status.textContent = 'Network error, please retry.'; + status.style.color = '#c0392b'; + }); + }); + }); + }); + + var playBtn = document.querySelector('.sim-detail-play'); + if (playBtn) { + playBtn.addEventListener('click', function () { + alert('Mirror snapshot — simulation playback is disabled in benchmark mode.'); + }); + } +})(); diff --git a/sites/phet_simulations/tasks.jsonl b/sites/phet_simulations/tasks.jsonl new file mode 100644 index 000000000..b60e8ed23 --- /dev/null +++ b/sites/phet_simulations/tasks.jsonl @@ -0,0 +1,43 @@ +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--0", "ques": "Find an interactive simulation about Newton's laws and forces that is suitable for elementary school students.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--1", "ques": "Search for a chemistry simulation that helps students learn how to balance chemical equations, and report which grade levels it targets.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--2", "ques": "Browse the Physics category on PhET and list three simulations that cover electromagnetism.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--3", "ques": "Find the 'Gravity and Orbits' simulation and report what subjects it is tagged with, what grade levels it targets, and when it was released.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--4", "ques": "Find a featured biology simulation about evolution and report its short description and target grade levels.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--5", "ques": "Search the PhET catalog for simulations matching 'DNA' and report how many results are returned.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Find a math simulation suitable for middle school students that covers probability, and report its play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--7", "ques": "Open the Translations page on PhET, find Chinese (Simplified), and report how many simulations are available in that language.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--8", "ques": "Find all simulations available in Arabic on PhET, and report whether any of them are tagged as 'New'.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--9", "ques": "List three languages PhET simulations are translated into that use a right-to-left script.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--10", "ques": "Filter the simulations catalog to show only Chemistry simulations for high school, sorted by most-played, and report the top result.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--11", "ques": "Find a teacher-submitted activity for the 'Build an Atom' simulation and report its author, grade level, and duration.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--12", "ques": "Browse the For Teachers section, find the most-downloaded activity, and report which simulation it is paired with.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--13", "ques": "Find an earth-science simulation that covers plate tectonics, and list the related simulations recommended on its detail page.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--14", "ques": "On the PhET About page, report the total number of simulations, subject areas, languages, and teacher activities listed.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--15", "ques": "Create a new teacher account on PhET with the email 'test_user@phet.test', then save the 'Energy Skate Park' simulation to your account.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--16", "ques": "Sign in to PhET as teacher@phet.test (password phet-teacher-pass) and report how many simulations are saved to that account.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--17", "ques": "Find a simulation about pH and acids/bases, and identify three subjects or topics it covers.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--18", "ques": "On the PhET catalog, find a simulation about photosynthesis and report which grade levels it targets and how long it takes to run.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--19", "ques": "Browse the Math category on PhET and find a simulation focused on graphing linear equations. Report its release date.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--20", "ques": "Find a chemistry simulation that helps students explore solutions and concentration, and list three related simulations.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--21", "ques": "Filter the catalog to show only simulations tagged 'New', and report how many are released in 2025.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--22", "ques": "Find the 'Circuit Construction Kit: DC' simulation and report the full list of languages it is available in.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--23", "ques": "Browse the biology category and report which simulations are appropriate for university-level students.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--24", "ques": "Look up an elementary-school physics simulation about magnets, and report its short description.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--25", "ques": "On the PhET For Teachers page, find a high-school activity longer than 50 minutes and report its title and author.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--26", "ques": "Find a simulation about the greenhouse effect or climate, and report its full overview paragraph.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--27", "ques": "Search for 'genetics' simulations on PhET and report which subjects are covered.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--28", "ques": "Find a simulation about waves and interference, and report what version it is currently at.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--29", "ques": "On the PhET catalog, sort simulations by newest first and list the five most recently released.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--30", "ques": "Filter the catalog to show only elementary-school simulations and report how many are available.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--31", "ques": "Find a biology simulation related to predator-prey or population dynamics and report its play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--32", "ques": "Browse the PhET catalog and identify a simulation that is tagged as both 'Physics' and 'Math'.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--33", "ques": "Open the Translations page, find Japanese, and click through to view all simulations available in Japanese.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--34", "ques": "Find the 'Natural Selection' simulation and report its target grade levels and three topics it covers.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--35", "ques": "Search for simulations about 'orbit' and report two that come up.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--36", "ques": "Find a teacher activity focused on equivalent fractions and report its target grade level and duration.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--37", "ques": "On the PhET catalog filter activities for high-school level only, and report how many are available.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--38", "ques": "Find a chemistry simulation about isotopes and report what grade levels and languages it supports.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--39", "ques": "Visit the Accessibility page on PhET and report what types of input methods are supported by the simulations.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--40", "ques": "Sign in as student@phet.test (password phet-student-pass), navigate to the 'Build an Atom' simulation, and save it to your account with a note.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--41", "ques": "Find the most-played simulation on the PhET homepage and report its title, subjects, and play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--42", "ques": "On the PhET catalog, find a simulation about the water cycle and report its target grade levels and short description.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} diff --git a/sites/phet_simulations/templates/404.html b/sites/phet_simulations/templates/404.html new file mode 100644 index 000000000..ac5e17182 --- /dev/null +++ b/sites/phet_simulations/templates/404.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Page not found · {{ site_title }}{% endblock %} +{% block content %} +
+

Page not found

+

The page you requested couldn't be found. Try one of these instead:

+ +
+{% endblock %} diff --git a/sites/phet_simulations/templates/_sim_card.html b/sites/phet_simulations/templates/_sim_card.html new file mode 100644 index 000000000..6e061aadc --- /dev/null +++ b/sites/phet_simulations/templates/_sim_card.html @@ -0,0 +1,18 @@ +
+ + {{ sim.title }} + +
+

+ {{ sim.title }} +

+

{{ sim.short_description }}

+
    + {% for subj_slug in sim.subjects() %} +
  • {{ subj_slug.replace('-', ' ').title() }}
  • + {% endfor %} +
+ {% if sim.is_new %}New{% endif %} + {% if sim.is_featured %}Featured{% endif %} +
+
diff --git a/sites/phet_simulations/templates/about.html b/sites/phet_simulations/templates/about.html new file mode 100644 index 000000000..55f184385 --- /dev/null +++ b/sites/phet_simulations/templates/about.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}About · {{ site_title }}{% endblock %} +{% block content %} +

About PhET

+ +
+

Founded in 2002 by Nobel laureate Carl Wieman, the PhET Interactive + Simulations project at the University of Colorado Boulder creates + free interactive math and science simulations. PhET simulations are + based on extensive education research and engage students through an + intuitive, game-like environment where they learn through exploration + and discovery.

+ +

By the numbers

+
    +
  • {{ stats.simulations }}simulations
  • +
  • {{ stats.subjects }}subject areas
  • +
  • {{ stats.languages }}languages
  • +
  • {{ stats.activities }}teacher activities
  • +
+ +

Mission

+

To advance science and math literacy and education worldwide through + free interactive simulations.

+ +

License

+

PhET simulations are freely available under a Creative Commons + Attribution license (CC-BY). The HTML5 simulations are downloadable + for offline use, embeddable into other course materials, and + translatable into any language.

+ +

About this snapshot

+

This deployment is a static mirror of the PhET catalog created for + the WebHarbor benchmark. Simulation play buttons are disabled in + this snapshot; visit the live site at phet.colorado.edu to launch + simulations.

+
+{% endblock %} diff --git a/sites/phet_simulations/templates/accessibility.html b/sites/phet_simulations/templates/accessibility.html new file mode 100644 index 000000000..b4327b397 --- /dev/null +++ b/sites/phet_simulations/templates/accessibility.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}Accessibility · {{ site_title }}{% endblock %} +{% block content %} +

Accessibility

+ +
+

PhET is committed to making its simulations accessible to as many + learners as possible. Accessibility features include:

+ +

Keyboard navigation

+

All HTML5 simulations support full keyboard navigation, including + tab order, arrow-key adjustments, and shortcut keys for common + interactions. Tab focus is visually indicated with a high-contrast outline.

+ +

Screen reader support

+

Simulations include semantic markup, ARIA labels, and self-voicing + descriptions that announce state changes and user actions for users + of screen readers such as NVDA, JAWS, and VoiceOver.

+ +

Visual customization

+

Users can adjust contrast and color settings via browser-level + preferences. Simulations respect the operating system's reduced-motion + setting and avoid flashing content above the WCAG 2.1 threshold.

+ +

Sound and captions

+

Where audio is used, simulations provide optional captions and the + ability to mute sound entirely without losing instructional content.

+ +

Translations

+

Visit the Translations page + to find simulations in over twenty-five languages including + right-to-left scripts.

+ +

Feedback

+

To report an accessibility issue or suggest improvements, contact + the PhET team at phethelp@colorado.edu.

+
+{% endblock %} diff --git a/sites/phet_simulations/templates/account.html b/sites/phet_simulations/templates/account.html new file mode 100644 index 000000000..f308cd220 --- /dev/null +++ b/sites/phet_simulations/templates/account.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %}My Account · {{ site_title }}{% endblock %} +{% block content %} +

Welcome, {{ current_user.name }}

+

+ {{ current_user.email }} + {% if current_user.institution %} · {{ current_user.institution }}{% endif %} + {% if current_user.country %} · {{ current_user.country }}{% endif %} + · Role: {{ current_user.role|capitalize }} +

+ +
+

Saved simulations ({{ saved_rows|length }})

+ {% if saved_rows %} +
    + {% for row in saved_rows %} +
  • +

    + {{ row.simulation.title }} +

    +

    {{ row.simulation.short_description }}

    + {% if row.notes %} +

    My notes: {{ row.notes }}

    + {% endif %} +

    Saved {{ row.saved_at.strftime('%B %d, %Y') }}

    +
  • + {% endfor %} +
+ {% else %} +

+ You haven't saved any simulations yet. + Browse simulations » +

+ {% endif %} +
+{% endblock %} diff --git a/sites/phet_simulations/templates/activities.html b/sites/phet_simulations/templates/activities.html new file mode 100644 index 000000000..500c692df --- /dev/null +++ b/sites/phet_simulations/templates/activities.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}Activities · {{ site_title }}{% endblock %} +{% block content %} + + +

Teacher-submitted activities

+

{{ activities|length }} activities published.

+ +
+ + + Reset +
+ +{% if activities %} +
    + {% for activity in activities %} +
  • +

    {{ activity.title }}

    +

    + For {{ activity.simulation.title }} · + {{ activity.grade_level|capitalize }} · + {{ activity.duration_min }} min +

    +

    {{ activity.description }}

    +

    By {{ activity.author }} · {{ activity.download_count }} downloads · {{ activity.published_date }}

    +
  • + {% endfor %} +
+{% else %} +

No activities for the selected filter.

+{% endif %} +{% endblock %} diff --git a/sites/phet_simulations/templates/activity_detail.html b/sites/phet_simulations/templates/activity_detail.html new file mode 100644 index 000000000..daf66e26f --- /dev/null +++ b/sites/phet_simulations/templates/activity_detail.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{% block title %}{{ activity.title }} · {{ site_title }}{% endblock %} +{% block content %} + + + +{% endblock %} diff --git a/sites/phet_simulations/templates/base.html b/sites/phet_simulations/templates/base.html new file mode 100644 index 000000000..7babf18d1 --- /dev/null +++ b/sites/phet_simulations/templates/base.html @@ -0,0 +1,100 @@ + + + + + + {% block title %}{{ site_title }}{% endblock %} + + + + +
+
+ + PhET + Interactive Simulations + + + +
+ {% if current_user.is_authenticated %} + {{ current_user.name }} + Sign out + {% else %} + Sign in + Sign up + {% endif %} +
+
+
+ +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, msg in messages %} +
{{ msg }}
+ {% endfor %} +
+ {% endif %} +{% endwith %} + +
+ {% block content %}{% endblock %} +
+ + + + + + diff --git a/sites/phet_simulations/templates/category.html b/sites/phet_simulations/templates/category.html new file mode 100644 index 000000000..6a590e6d8 --- /dev/null +++ b/sites/phet_simulations/templates/category.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% block title %}{{ subject.name }} Simulations · {{ site_title }}{% endblock %} +{% block content %} +
+

{{ subject.name }}

+

{{ subject.description }}

+

{{ sims|length }} simulation{{ '' if sims|length == 1 else 's' }} in this category.

+
+ +{% if sims %} +
+ {% for sim in sims %} + {% include "_sim_card.html" %} + {% endfor %} +
+{% else %} +

No simulations available in this category yet.

+{% endif %} +{% endblock %} diff --git a/sites/phet_simulations/templates/index.html b/sites/phet_simulations/templates/index.html new file mode 100644 index 000000000..ad38d06a1 --- /dev/null +++ b/sites/phet_simulations/templates/index.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Free interactive math & science simulations

+

Research-based simulations from the University of Colorado Boulder. + Browse {{ total_simulations }} simulations across physics, chemistry, + math, biology, and earth science — available in {{ total_languages }} languages.

+

+ Browse all simulations + For teachers +

+
+
+ +
+ {% for subject in primary_subjects %} + + + {{ subject.name }} + {{ subject.description }} + + {% endfor %} +
+ +
+

Featured simulations

+
+ {% for sim in featured %} + {% include "_sim_card.html" %} + {% endfor %} +
+
+ +
+

New & recently updated

+
+ {% for sim in new_sims %} + {% include "_sim_card.html" %} + {% endfor %} +
+
+ +
+

Most played this month

+
+ {% for sim in most_played %} + {% include "_sim_card.html" %} + {% endfor %} +
+
+{% endblock %} diff --git a/sites/phet_simulations/templates/login.html b/sites/phet_simulations/templates/login.html new file mode 100644 index 000000000..f4ff3a51b --- /dev/null +++ b/sites/phet_simulations/templates/login.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}Sign In · {{ site_title }}{% endblock %} +{% block content %} +
+

Sign in to PhET

+

Sign in to save simulations, write notes, and access teacher resources.

+ +
+ + + + +
+ +

+ Don't have an account? Create one now. +

+
+{% endblock %} diff --git a/sites/phet_simulations/templates/register.html b/sites/phet_simulations/templates/register.html new file mode 100644 index 000000000..810726143 --- /dev/null +++ b/sites/phet_simulations/templates/register.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block title %}Create Account · {{ site_title }}{% endblock %} +{% block content %} +
+

Create your PhET account

+

A free account lets you save simulations and download teacher resources.

+ +
+ + + + + + + +
+ +

+ Already have an account? Sign in. +

+
+{% endblock %} diff --git a/sites/phet_simulations/templates/search.html b/sites/phet_simulations/templates/search.html new file mode 100644 index 000000000..95d69c4a4 --- /dev/null +++ b/sites/phet_simulations/templates/search.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}{% if query %}Search: {{ query }}{% else %}Search{% endif %} · {{ site_title }}{% endblock %} +{% block content %} +

Search

+ + + +{% if query %} +

{{ total }} result{{ '' if total == 1 else 's' }} for “{{ query }}”

+ {% if sims %} +
+ {% for sim in sims %} + {% include "_sim_card.html" %} + {% endfor %} +
+ {% else %} +

No simulations matched your search. Try a shorter or more general term.

+ {% endif %} +{% else %} +

Enter a search term to find simulations by title or topic.

+{% endif %} +{% endblock %} diff --git a/sites/phet_simulations/templates/simulation_detail.html b/sites/phet_simulations/templates/simulation_detail.html new file mode 100644 index 000000000..35f42841d --- /dev/null +++ b/sites/phet_simulations/templates/simulation_detail.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} +{% block title %}{{ sim.title }} · {{ site_title }}{% endblock %} +{% block content %} + + +
+
+

{{ sim.title }}

+

{{ sim.short_description }}

+
    + {% if sim.is_html5 %}
  • HTML5
  • {% endif %} + {% if sim.is_new %}
  • New
  • {% endif %} + {% if sim.is_featured %}{% endif %} +
  • v{{ sim.version }}
  • +
+
+ +
+
+
+ {{ sim.title }} + +
+ +
+

About this simulation

+

{{ sim.overview }}

+
+ +
+

Learning goals

+
    + {% for topic in sim.topics() %} +
  • {{ topic|capitalize }}
  • + {% endfor %} +
+
+ + {% if activities %} +
+

Teacher-submitted activities ({{ activities|length }})

+
    + {% for activity in activities %} +
  • + {{ activity.title }} + by {{ activity.author }} · {{ activity.grade_level|capitalize }} · {{ activity.duration_min }} min +
  • + {% endfor %} +
+
+ {% endif %} + + {% if related %} +
+

Related simulations

+
+ {% for sim in related %} + {% include "_sim_card.html" %} + {% endfor %} +
+
+ {% endif %} +
+ + +
+
+{% endblock %} diff --git a/sites/phet_simulations/templates/simulations.html b/sites/phet_simulations/templates/simulations.html new file mode 100644 index 000000000..42f2f2aef --- /dev/null +++ b/sites/phet_simulations/templates/simulations.html @@ -0,0 +1,68 @@ +{% extends "base.html" %} +{% block title %}All Simulations · {{ site_title }}{% endblock %} +{% block content %} +

All Simulations

+

{{ total }} simulation{{ '' if total == 1 else 's' }} matching your filters.

+ +
+ + + + + + Reset +
+ +{% if sims %} +
+ {% for sim in sims %} + {% include "_sim_card.html" %} + {% endfor %} +
+ + {% if total_pages > 1 %} + + {% endif %} +{% else %} +

No simulations match the selected filters. Try clearing one of the filters above.

+{% endif %} +{% endblock %} diff --git a/sites/phet_simulations/templates/teachers.html b/sites/phet_simulations/templates/teachers.html new file mode 100644 index 000000000..f1d853d2d --- /dev/null +++ b/sites/phet_simulations/templates/teachers.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}For Teachers · {{ site_title }}{% endblock %} +{% block content %} +
+

Resources for teachers

+

Lesson plans, activities, and tips for using PhET simulations in your classroom. + Activities are contributed by educators and aligned to common curriculum standards.

+

+ Browse all activities + Find a simulation +

+
+ +
+

Most-downloaded activities

+
    + {% for activity in featured_activities %} +
  • +

    {{ activity.title }}

    +

    + For {{ activity.simulation.title }} · + {{ activity.grade_level|capitalize }} · + {{ activity.duration_min }} min · + {{ activity.file_type }} +

    +

    {{ activity.description }}

    +

    By {{ activity.author }} · {{ activity.download_count }} downloads

    +
  • + {% endfor %} +
+
+{% endblock %} diff --git a/sites/phet_simulations/templates/translation_detail.html b/sites/phet_simulations/templates/translation_detail.html new file mode 100644 index 000000000..59aacfe40 --- /dev/null +++ b/sites/phet_simulations/templates/translation_detail.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}{{ language.name }} translations · {{ site_title }}{% endblock %} +{% block content %} + + +

{{ language.name }} ({{ language.native_name }})

+

+ {{ sims|length }} simulations available in {{ language.name }}. + Language code: {{ language.code }}{% if language.is_rtl %} · right-to-left script{% endif %}. +

+ +{% if sims %} +
+ {% for sim in sims %} + {% include "_sim_card.html" %} + {% endfor %} +
+{% else %} +

No simulations are translated to this language yet.

+{% endif %} +{% endblock %} diff --git a/sites/phet_simulations/templates/translations.html b/sites/phet_simulations/templates/translations.html new file mode 100644 index 000000000..889467cec --- /dev/null +++ b/sites/phet_simulations/templates/translations.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block title %}Translations · {{ site_title }}{% endblock %} +{% block content %} +

Translations

+

+ PhET simulations are translated into {{ languages|length }} languages by educators and volunteers worldwide. + Select a language to see which simulations are available. +

+ + +{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index 941bf3231..c63955a7d 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -7,7 +7,7 @@ SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted osu rotten_tomatoes compass walmart_careers - fedex webmd_doctor healthline kaggle nvidia) + fedex webmd_doctor healthline kaggle nvidia phet_simulations) BASE_PORT=40000 SITE_COUNT=${#SITES[@]} PID_DIR=/tmp/websyn_pids From 78715557a0dae1fe96e2fe3b99830d27bc13bbee Mon Sep 17 00:00:00 2001 From: ltom01241010 Date: Mon, 18 May 2026 10:02:44 -0400 Subject: [PATCH 02/10] feat(phet_simulations): reskin to match phet.colorado.edu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace placeholder UI with a high-fidelity mirror of the real PhET Interactive Simulations site. Visual: - Real PhET yellow trademark logo + CU Boulder logo in white header bar - Right-aligned nav (Simulations / Studio / Teaching / Research / Initiatives) with hover dropdowns; collapsing search icon + profile icon - Dismissible pink educator banner - Photo-backed top hero ("Interactive Simulations for Science and Math") - 5 pastel subject squares in PhET's canonical order (Physics, Math & Statistics, Chemistry, Earth & Space, Biology) with their SVG icons - Photo-backed Teaching Resources callout - "Interact. Discover. Learn." stats section with real iconography - Secondary STEM hero photo + centered "Supported in part by" + Donate CTA - Real footer: social row, 4-column links, language selector, app-store badges, sponsor logos strip (Moore, Hewlett, NSF, Mastercard, Yidan) Simulations browse page: - Underwater illustration banner + Browse / Filter / Customize sub-tabs - Filter view is the default; left sidebar with collapsible subject tree, grade level, compatibility, release type, inclusive features, locale, apply / clear, active-filter chips, sort dropdown, results count - Browse view groups sims by subject with "View all »" links Simulation detail page: - Large screenshot with hover "Play" overlay - Tabbed sections (About / Teaching Resources / Activities / Translations / Credits) plus topics, learning goals, related sims - "Supported by" sidebar, share row, pink Explore More + Donate CTA Backend wiring: - Index route adds total_activities for the stats section - Simulations route exposes view tab and per-subject buckets - App.py exposes available_thumbnails so cards fall back gracefully when a slug has no screenshot Assets ship via the paired HF dataset tarball (not committed): - 98 sim screenshots (49 from PhET CDN, 49 generated placeholders) - PhET + CU Boulder logos, subject icons, sponsor logos, hero photos, app-store badges, underwater simulations banner Seed DB unchanged, /reset/phet_simulations remains byte-identical (md5 e094a2ee23369d3b60232f49f4ac691c). Co-Authored-By: Claude Opus 4.7 --- sites/phet_simulations/app.py | 31 +- sites/phet_simulations/static/css/style.css | 1202 ++++++++++++++--- .../phet_simulations/templates/_sim_card.html | 24 +- sites/phet_simulations/templates/base.html | 198 ++- sites/phet_simulations/templates/index.html | 134 +- .../templates/simulation_detail.html | 103 +- .../templates/simulations.html | 260 +++- 7 files changed, 1567 insertions(+), 385 deletions(-) diff --git a/sites/phet_simulations/app.py b/sites/phet_simulations/app.py index 0d8655086..34bf42f40 100644 --- a/sites/phet_simulations/app.py +++ b/sites/phet_simulations/app.py @@ -189,6 +189,13 @@ def _saved_sim_ids(): return {s.sim_id for s in current_user.saved.all()} +_THUMB_DIR = os.path.join(BASE_DIR, "static", "images", "sims") +_AVAILABLE_THUMBNAILS = frozenset( + f[:-4] for f in os.listdir(_THUMB_DIR) + if f.endswith(".png") and os.path.getsize(os.path.join(_THUMB_DIR, f)) > 1000 +) if os.path.isdir(_THUMB_DIR) else frozenset() + + @app.context_processor def inject_globals(): return { @@ -198,6 +205,7 @@ def inject_globals(): "primary_subjects": Subject.query.order_by(Subject.name).all(), "grade_levels": GradeLevel.query.order_by(GradeLevel.sort_order).all(), "saved_sim_ids": _saved_sim_ids(), + "available_thumbnails": _AVAILABLE_THUMBNAILS, } @@ -231,6 +239,7 @@ def index(): ) total = Simulation.query.count() total_languages = Language.query.count() + total_activities = Activity.query.count() return render_template( "index.html", featured=featured, @@ -238,6 +247,7 @@ def index(): most_played=most_played, total_simulations=total, total_languages=total_languages, + total_activities=total_activities, ) @@ -247,8 +257,9 @@ def simulations(): grade = request.args.get("grade", "").strip() language = request.args.get("language", "").strip() sort = request.args.get("sort", "title") + view = request.args.get("view", "filter").strip() page = max(int(request.args.get("page", 1)), 1) - per_page = 12 + per_page = 24 query = Simulation.query if subject: @@ -269,6 +280,21 @@ def simulations(): sims = query.offset((page - 1) * per_page).limit(per_page).all() total_pages = max((total + per_page - 1) // per_page, 1) + any_filter_active = bool(subject or grade or language or sort != "title") + view_tab = view if view in ("browse", "filter", "customize") else "filter" + + sims_by_subject = {} + if view_tab == "browse": + ordered = ['physics', 'math', 'chemistry', 'earth-science', 'biology'] + for slug in ordered: + sims_by_subject[slug] = ( + Simulation.query + .filter(Simulation.subjects_json.like(f'%"{slug}"%')) + .order_by(Simulation.title) + .limit(7) + .all() + ) + return render_template( "simulations.html", sims=sims, @@ -279,7 +305,10 @@ def simulations(): grade=grade, language=language, sort=sort, + view_tab=view_tab, languages=Language.query.order_by(Language.name).all(), + any_filter_active=any_filter_active, + sims_by_subject=sims_by_subject, ) diff --git a/sites/phet_simulations/static/css/style.css b/sites/phet_simulations/static/css/style.css index b55935f13..5be3fab3b 100644 --- a/sites/phet_simulations/static/css/style.css +++ b/sites/phet_simulations/static/css/style.css @@ -1,291 +1,989 @@ +/* PhET Interactive Simulations — mirror reskin + Palette + typography derived from phet.colorado.edu (CC-BY) */ + :root { + --phet-magenta: #e01e5a; + --phet-magenta-dark: #b6184a; + --phet-navy: #2a326a; + --phet-navy-dark: #1f2552; + --phet-blue: #15337f; + --phet-blue-hover: #2345a1; + --phet-orange: #d36a04; + --phet-purple: #521764; + --phet-cyan: #00a1cc; + --c-bg: #ffffff; - --c-bg-alt: #f4f6f8; + --c-bg-alt: #f3f5f3; + --c-bg-footer: #f0f0f0; --c-border: #d8dde3; + --c-border-strong: #b8bfc6; --c-text: #1f2933; --c-muted: #5b6470; - --c-primary: #0079bf; - --c-primary-dark: #005a8c; - --c-accent: #f5862e; + --c-link: var(--phet-blue); + --c-link-hover: var(--phet-magenta); --c-success: #2e7d32; --c-error: #c0392b; - --c-green: #6cba5c; - --radius: 6px; - --shadow: 0 1px 3px rgba(0, 0, 0, 0.08); - --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.12); + + --radius: 3px; + --shadow-card: 1px 1px 4px rgba(0,0,0,0.25); + --shadow-card-hover: 2px 2px 8px rgba(0,0,0,0.35); --max-width: 1180px; } * { box-sizing: border-box; } html, body { margin: 0; padding: 0; } body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Helvetica Neue", Arial, sans-serif; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: var(--c-text); background: var(--c-bg); line-height: 1.5; + font-size: 14px; } -a { color: var(--c-primary); text-decoration: none; } -a:hover { text-decoration: underline; } +a { color: var(--c-link); text-decoration: none; } +a:hover { color: var(--c-link-hover); text-decoration: underline; } +h1, h2, h3, h4 { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } .container { max-width: var(--max-width); margin: 0 auto; padding: 0 1.25rem; } -/* Topbar */ -.topbar { - background: var(--c-primary-dark); - color: #fff; - padding: 0.75rem 0; - border-bottom: 3px solid var(--c-accent); -} +/* === Header (real PhET look: single white bar) === */ +.topbar { background: #fff; border-bottom: 1px solid var(--c-border); } .topbar-inner { + display: flex; align-items: center; gap: 1.5rem; + padding: 0.5rem 1.25rem; + max-width: var(--max-width); margin: 0 auto; + min-height: 60px; +} +.brand { display: flex; align-items: center; gap: 0.65rem; text-decoration: none; flex-shrink: 0; } +.brand:hover { text-decoration: none; } +.brand-phet { height: 42px; width: auto; display: block; } +.brand-cu { height: 38px; width: auto; display: block; } +.brand-divider { + width: 1px; height: 36px; background: var(--c-border); + display: inline-block; +} + +.primary-nav { display: flex; - align-items: center; - gap: 1.25rem; - flex-wrap: wrap; + gap: 0; + margin-left: auto; + align-items: stretch; } -.brand { display: flex; align-items: center; gap: 0.5rem; color: #fff; text-decoration: none; } -.brand-mark { - background: var(--c-accent); - color: #fff; - padding: 0.25rem 0.5rem; - border-radius: var(--radius); +.nav-item { + position: relative; + display: flex; align-items: center; +} +.nav-item > a { + color: #4a525c; + text-transform: uppercase; font-weight: 700; - letter-spacing: 0.05em; + font-size: 0.78rem; + letter-spacing: 0.06em; + padding: 1.1rem 0.95rem; + text-decoration: none; + display: block; + white-space: nowrap; +} +.nav-item > a:hover { color: var(--phet-magenta); text-decoration: none; } +.nav-item.active > a { color: var(--phet-magenta); } +.nav-item.active > a::before { + content: ""; position: absolute; + top: 0; left: 50%; transform: translateX(-50%); + width: 22px; height: 3px; + background: var(--phet-magenta); +} +.nav-dropdown { + display: none; + position: absolute; + top: 100%; left: 0; + background: #fff; + border: 1px solid var(--c-border); + box-shadow: 0 4px 14px rgba(0,0,0,0.12); + min-width: 180px; + padding: 0.5rem 0; + z-index: 100; +} +.nav-item:hover .nav-dropdown, +.nav-item:focus-within .nav-dropdown { display: block; } +.nav-dropdown a { + display: block; + padding: 0.55rem 1.1rem; + color: var(--c-text); + font-size: 0.88rem; + text-decoration: none; + text-transform: none; + letter-spacing: 0; + font-weight: 400; +} +.nav-dropdown a:hover { background: var(--c-bg-alt); color: var(--phet-magenta); } +.nav-dropdown hr { + border: 0; border-top: 1px solid var(--c-border); + margin: 0.4rem 0; } -.brand-text { font-weight: 600; font-size: 1.05rem; } -.primary-nav { display: flex; gap: 1rem; flex: 1 1 auto; } -.primary-nav a { color: #fff; font-size: 0.95rem; } -.primary-nav a:hover { text-decoration: underline; } -.search-form { display: flex; gap: 0.25rem; } +.search-form { + display: flex; gap: 0; + margin-left: 0.5rem; + align-items: center; + position: relative; +} .search-form input[type=search] { - padding: 0.4rem 0.6rem; - border: 1px solid var(--c-border); - border-radius: var(--radius); - min-width: 200px; + position: absolute; + right: 100%; + top: 50%; transform: translateY(-50%); + padding: 0.35rem 0.55rem; + border: 1px solid var(--c-border-strong); + border-right: 0; + border-radius: var(--radius) 0 0 var(--radius); + width: 0; padding-left: 0; padding-right: 0; + font-size: 0.85rem; + outline: none; + background: #fff; + visibility: hidden; + transition: width 0.18s, padding 0.18s; +} +.search-form:hover input[type=search], +.search-form:focus-within input[type=search] { + width: 200px; + padding-left: 0.55rem; padding-right: 0.55rem; + visibility: visible; } +.search-form input[type=search]:focus { border-color: var(--phet-magenta); } .search-form button { - background: var(--c-accent); - color: #fff; + background: transparent; + color: var(--c-text); border: 0; - padding: 0.4rem 0.8rem; + padding: 0.5rem; border-radius: var(--radius); cursor: pointer; + display: flex; align-items: center; } +.search-form button:hover { color: var(--phet-magenta); } .user-menu { display: flex; gap: 0.5rem; align-items: center; } -.user-link { color: #fff; font-size: 0.9rem; } -.user-link.muted { opacity: 0.7; } -.btn-mini { - background: var(--c-accent); - padding: 0.25rem 0.6rem; - border-radius: var(--radius); +.user-link { + color: var(--c-muted); + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; + display: flex; align-items: center; + padding: 0.3rem 0.5rem; } +.user-link:hover { color: var(--phet-magenta); text-decoration: none; } +.user-link.muted { font-weight: 400; } /* Flashes */ .flashes { margin-top: 1rem; } .flash { - padding: 0.75rem 1rem; - margin-bottom: 0.5rem; - border-radius: var(--radius); - border: 1px solid; + padding: 0.75rem 1rem; margin-bottom: 0.5rem; + border-radius: var(--radius); border: 1px solid; } .flash-success { background: #e8f5e9; border-color: var(--c-success); color: var(--c-success); } .flash-error { background: #fdecea; border-color: var(--c-error); color: var(--c-error); } /* Layout */ -.main { padding: 2rem 0 3rem; min-height: 60vh; } -.page-title { margin: 0 0 0.5rem; font-size: 1.75rem; } -.page-subtitle { color: var(--c-muted); margin: 0 0 1.5rem; } +.main { padding: 1.5rem 0 3rem; } +.main:not(:empty) { min-height: 40vh; } +.home-main { padding: 1.5rem 0 2rem; } +.page-title { margin: 0 0 0.4rem; font-size: 1.65rem; color: var(--phet-orange); font-weight: normal; } +.page-subtitle { color: var(--c-muted); margin: 0 0 1.5rem; font-size: 0.95rem; } .row { margin: 2rem 0; } -.row-title { font-size: 1.3rem; margin: 0 0 1rem; border-bottom: 2px solid var(--c-bg-alt); padding-bottom: 0.35rem; } -.empty-state { background: var(--c-bg-alt); padding: 1rem 1.25rem; border-radius: var(--radius); color: var(--c-muted); } -.breadcrumb { color: var(--c-muted); font-size: 0.9rem; margin-bottom: 0.5rem; } -.breadcrumb a { color: var(--c-primary); } +.row-title { + font-size: 1.15rem; margin: 0 0 1rem; + color: var(--phet-blue); font-weight: 700; + border-bottom: 2px solid var(--phet-blue); padding-bottom: 0.35rem; +} +.empty-state { + background: var(--c-bg-alt); padding: 1rem 1.25rem; + border-radius: var(--radius); color: var(--c-muted); +} +.breadcrumb { color: var(--c-muted); font-size: 0.88rem; margin-bottom: 0.75rem; } +.breadcrumb a { color: var(--phet-blue); } +/* Buttons — PhET style: small, uppercase, bold */ .btn { display: inline-block; - padding: 0.5rem 1rem; + padding: 0.45rem 0.95rem; border-radius: var(--radius); - font-weight: 600; + font-weight: 700; cursor: pointer; border: 1px solid transparent; text-decoration: none; - font-size: 0.95rem; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.05em; + line-height: 1.2; +} +.btn-primary { background: var(--phet-navy); color: #fff; } +.btn-primary:hover { background: var(--phet-navy-dark); text-decoration: none; color: #fff; } +.btn-ghost { + background: #fff; color: var(--phet-navy); + border-color: var(--phet-navy); } -.btn-primary { background: var(--c-primary); color: #fff; } -.btn-primary:hover { background: var(--c-primary-dark); text-decoration: none; } -.btn-ghost { background: #fff; color: var(--c-primary); border-color: var(--c-primary); } +.btn-ghost:hover { background: var(--phet-navy); color: #fff; text-decoration: none; } .btn-block { display: block; width: 100%; text-align: center; } +.btn-play { + background: var(--phet-magenta); color: #fff; font-size: 0.9rem; + padding: 0.65rem 1.4rem; +} +.btn-play:hover { background: var(--phet-magenta-dark); color: #fff; } + +/* === Educator banner (thin pink strip above hero) === */ +.educator-banner { + background: #fde4ee; + color: #6b1734; + font-size: 0.85rem; + padding: 0.55rem 2.5rem 0.55rem 1.25rem; + text-align: center; + border-bottom: 1px solid #f8cfde; + position: relative; +} +.educator-banner a { color: var(--phet-magenta); font-weight: 700; text-decoration: underline; } +.educator-close { + position: absolute; + right: 1rem; top: 50%; transform: translateY(-50%); + background: transparent; + border: 0; + color: #6b1734; + cursor: pointer; + padding: 0.3rem; + display: flex; align-items: center; + border-radius: 50%; + line-height: 0; +} +.educator-close:hover { background: rgba(0,0,0,0.05); color: var(--phet-magenta); } + + +/* === Hero (real photo top banner) === */ +.hero-photo { + position: relative; + background: + linear-gradient(90deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.4) 60%, rgba(0,0,0,0.5) 100%), + url('../images/ui/hero-simulations.jpg') center/cover no-repeat; + color: #fff; + min-height: 420px; + display: flex; align-items: center; + margin: 0; + overflow: hidden; +} +.hero-photo-inner { + position: relative; + max-width: var(--max-width); margin: 0 auto; + padding: 4rem 1.25rem; + width: 100%; + display: flex; justify-content: flex-end; +} +.hero-photo-text { max-width: 540px; text-align: left; } +.hero-photo h1 { + font-size: 2.4rem; margin: 0 0 1.5rem; + font-weight: normal; line-height: 1.15; + text-shadow: 0 2px 12px rgba(0,0,0,0.6); +} +.hero-photo .btn { + background: rgba(0,0,0,0.65); + border: 1px solid #fff; + color: #fff; + border-radius: 10px; + padding: 0.75rem 1.5rem; + font-size: 0.82rem; +} +.hero-photo .btn:hover { background: var(--phet-magenta); border-color: var(--phet-magenta); color: #fff; text-decoration: none; } + +/* Secondary hero — "Shouldn't All Students Experience STEM?" */ +.hero-stem { + position: relative; + background: + linear-gradient(90deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.4) 60%, rgba(0,0,0,0.55) 100%), + url('../images/ui/hero-accessibility.jpg') center/cover no-repeat; + color: #fff; + min-height: 320px; + display: flex; align-items: center; + margin: 0; +} +.hero-stem-inner { + max-width: var(--max-width); margin: 0 auto; + padding: 3rem 1.25rem; + width: 100%; + display: flex; justify-content: flex-end; +} +.hero-stem-text { max-width: 460px; text-align: left; } +.hero-stem h2 { + font-size: 1.8rem; margin: 0 0 0.8rem; + font-weight: normal; line-height: 1.2; + text-shadow: 0 2px 12px rgba(0,0,0,0.6); +} +.hero-stem p { margin: 0 0 1.25rem; opacity: 0.95; font-size: 0.92rem; } +.hero-stem .btn { + background: rgba(0,0,0,0.6); + border: 1px solid #fff; + color: #fff; + border-radius: 10px; + padding: 0.65rem 1.3rem; + font-size: 0.78rem; +} +.hero-stem .btn:hover { background: var(--phet-magenta); border-color: var(--phet-magenta); color: #fff; } + +/* Home-page "Supported by" + DONATE NOW block */ +.home-supported { + background: #fff; + text-align: center; + padding: 2.5rem 1.25rem 3rem; + border-top: 1px solid var(--c-border); +} +.home-supported-label { + font-size: 0.85rem; color: var(--c-text); + margin-bottom: 1rem; +} +.home-supported-logo { + font-family: Georgia, serif; font-style: italic; + font-size: 1.3rem; color: var(--phet-navy); font-weight: 700; + display: inline-block; margin-bottom: 0.5rem; +} +.home-supported-foot { + font-size: 0.85rem; color: var(--c-muted); + margin: 0.5rem auto 1.25rem; max-width: 480px; +} +.home-supported .donate-cta { margin-top: 0; } -/* Hero */ +/* "Over 1.8 billion simulations delivered" banner */ +.stats-banner { + background: #f0f0f0; + padding: 1rem 1.25rem; + text-align: center; + font-size: 1rem; + color: var(--c-text); + font-weight: 600; + border-bottom: 1px solid var(--c-border); +} +.stats-banner strong { color: var(--phet-magenta); font-weight: 800; } + +/* legacy .hero retained for sub-pages (orange accent) */ .hero { - background: linear-gradient(135deg, #e8f1f8 0%, #d9e9f3 100%); - padding: 2.5rem 2rem; - border-radius: var(--radius); + background: var(--c-bg-alt); + padding: 2rem; + border-left: 5px solid var(--phet-magenta); margin-bottom: 2rem; } -.hero h1 { margin: 0 0 0.75rem; font-size: 2rem; } +.hero h1 { margin: 0 0 0.5rem; font-size: 1.8rem; color: var(--phet-orange); font-weight: normal; } .hero p { margin: 0 0 1rem; max-width: 720px; } .hero-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; } -/* Subject tiles */ -.subjects-grid { +/* === Pastel subject squares (homepage) === */ +.subjects-pastel { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + grid-template-columns: repeat(5, 1fr); gap: 1rem; - margin: 1.5rem 0 2.5rem; + margin: 2rem 0; } -.subject-tile { - display: flex; - flex-direction: column; +@media (max-width: 760px) { .subjects-pastel { grid-template-columns: repeat(2, 1fr); } } +.subject-pastel { + aspect-ratio: 1; + border-radius: 14px; + background: var(--ps-bg, #eee); + display: flex; flex-direction: column; + align-items: center; justify-content: center; + text-decoration: none; color: var(--c-text); padding: 1.25rem; - border-radius: var(--radius); - border: 1px solid var(--c-border); - border-top: 4px solid var(--tile-color, var(--c-primary)); - background: #fff; + transition: transform 0.18s; +} +.subject-pastel:hover { transform: translateY(-3px); text-decoration: none; } +.subject-pastel img { width: 56px; height: 56px; margin-bottom: 0.85rem; } +.subject-pastel-name { + font-weight: 700; font-size: 0.85rem; + text-transform: uppercase; letter-spacing: 0.05em; + text-align: center; color: var(--c-text); - transition: transform 0.15s, box-shadow 0.15s; } -.subject-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-lg); text-decoration: none; } -.subject-icon { - width: 40px; height: 40px; - border-radius: 50%; - background: var(--tile-color, var(--c-primary)); +/* pastel color palette by subject slug */ +.ps-physics { background: #f7d6da; } +.ps-math { background: #fdeec0; } +.ps-chemistry { background: #cbe9f3; } +.ps-earth-science { background: #d4e8c8; } +.ps-biology { background: #e7d4ea; } + +/* === Teaching Resources dark photo callout === */ +.teaching-callout { + position: relative; + background: + linear-gradient(90deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.5) 45%, rgba(0,0,0,0.1) 100%), + url('../images/ui/hero-teachers.jpg') center/cover no-repeat; color: #fff; + padding: 3.5rem 2rem; + margin: 0; + min-height: 320px; + display: flex; align-items: center; + overflow: hidden; +} +.teaching-callout-inner { + max-width: var(--max-width); margin: 0 auto; + width: 100%; +} +.teaching-callout-text { max-width: 460px; } +.teaching-callout h2 { + font-size: 1.9rem; margin: 0 0 0.85rem; + font-weight: normal; line-height: 1.2; + text-shadow: 0 2px 8px rgba(0,0,0,0.4); +} +.teaching-callout p { margin: 0 0 1.25rem; opacity: 0.95; font-size: 0.9rem; line-height: 1.5; } +.teaching-callout .btn { + background: rgba(0,0,0,0.6); + border: 1px solid #fff; color: #fff; + border-radius: 10px; padding: 0.65rem 1.5rem; + font-size: 0.78rem; +} +.teaching-callout .btn:hover { background: var(--phet-magenta); border-color: var(--phet-magenta); color: #fff; } + +/* === INTERACT DISCOVER LEARN stats row === */ +.interact-section { + text-align: center; + margin: 3rem 0; +} +.interact-heading img { + height: 80px; margin: 0 auto 1.5rem; + display: block; +} +.interact-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; + max-width: 800px; + margin: 1rem auto 2rem; +} +@media (max-width: 700px) { .interact-stats { grid-template-columns: 1fr; } } +.interact-stat { text-align: center; } +.interact-stat-num { + font-size: 2.4rem; font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0.6rem; + color: var(--c-text); + display: block; margin-bottom: 0.25rem; +} +.interact-stat-label { + color: var(--c-muted); + font-size: 0.95rem; +} +.interact-stat img { height: 40px; margin-top: 0.5rem; } + +/* About PhET paragraph */ +.about-blurb { + max-width: 740px; + margin: 2rem auto 3rem; + text-align: center; + color: var(--c-muted); + font-size: 0.95rem; + line-height: 1.6; } -.subject-name { font-weight: 700; font-size: 1.05rem; } -.subject-desc { color: var(--c-muted); font-size: 0.88rem; margin-top: 0.25rem; } +.about-blurb a { color: var(--phet-magenta); } -/* Sim card */ +/* === Sim card === */ .sim-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 1.25rem; + grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); + gap: 1.5rem 1rem; } .sim-grid-small { - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 1rem; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 1.25rem 0.85rem; } .sim-card { - background: #fff; - border: 1px solid var(--c-border); - border-radius: var(--radius); - overflow: hidden; - display: flex; - flex-direction: column; + background: transparent; + display: flex; flex-direction: column; position: relative; - transition: box-shadow 0.15s, transform 0.15s; + border: 0; } -.sim-card:hover { box-shadow: var(--shadow-lg); transform: translateY(-1px); } .sim-thumb { - background: linear-gradient(135deg, #0079bf 0%, #6cba5c 100%); - aspect-ratio: 16 / 10; - display: flex; - align-items: center; - justify-content: center; + display: block; + position: relative; + aspect-ratio: 3 / 2; + overflow: hidden; + box-shadow: var(--shadow-card); + background: #f4f6f8; + margin-bottom: 6px; +} +.sim-thumb:hover { box-shadow: var(--shadow-card-hover); } +.sim-thumb img { + width: 100%; height: 100%; + object-fit: cover; + display: block; +} +.sim-thumb-placeholder { + width: 100%; height: 100%; + display: flex; align-items: center; justify-content: center; + background: linear-gradient(135deg, var(--ph-color-a, #2a326a) 0%, var(--ph-color-b, #15337f) 100%); color: #fff; - font-weight: 700; + font-weight: 600; + font-size: 0.85rem; text-align: center; - padding: 0.5rem; - text-decoration: none; + padding: 0.6rem; + line-height: 1.2; } -.sim-thumb-label { font-size: 0.95rem; line-height: 1.2; } -.sim-meta { padding: 0.75rem 0.9rem 1rem; flex: 1; display: flex; flex-direction: column; } -.sim-title { margin: 0 0 0.25rem; font-size: 1rem; } -.sim-title a { color: var(--c-text); } -.sim-title a:hover { color: var(--c-primary); text-decoration: underline; } -.sim-desc { color: var(--c-muted); font-size: 0.85rem; margin: 0 0 0.5rem; flex: 1; } -.sim-tags { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 0.25rem; } -.sim-tag { font-size: 0.72rem; background: var(--c-bg-alt); color: var(--c-muted); padding: 0.15rem 0.4rem; border-radius: 3px; } -.badge { display: inline-block; font-size: 0.7rem; padding: 0.15rem 0.45rem; border-radius: 3px; margin-right: 0.25rem; } -.badge-new { background: var(--c-accent); color: #fff; } -.badge-featured { background: var(--c-green); color: #fff; } -.badge-html5 { background: var(--c-primary); color: #fff; } +.sim-type-badge { + position: absolute; + bottom: 6px; left: 6px; + width: 26px; height: 26px; + border-radius: 50%; + background: var(--phet-magenta); + color: #fff; + font-size: 0.65rem; + font-weight: 800; + display: flex; align-items: center; justify-content: center; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); +} +.sim-meta { padding: 0; } +.sim-title { margin: 0 0 0.2rem; font-size: 0.92rem; font-weight: 600; line-height: 1.25; } +.sim-title a { color: var(--c-text); text-decoration: none; } +.sim-title a:hover { color: var(--phet-magenta); text-decoration: underline; } +.sim-desc { color: var(--c-muted); font-size: 0.78rem; margin: 0 0 0.3rem; line-height: 1.35; } +.sim-tags { list-style: none; padding: 0; margin: 0.25rem 0 0; display: flex; flex-wrap: wrap; gap: 0.25rem; } +.sim-tag { + font-size: 0.68rem; + background: transparent; + color: var(--phet-blue); + padding: 0; +} +.sim-tag::after { content: ","; } +.sim-tag:last-child::after { content: ""; } +.badge { + display: inline-block; font-size: 0.65rem; + padding: 0.1rem 0.45rem; border-radius: 2px; + margin-right: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 700; +} +.badge-new { background: var(--phet-magenta); color: #fff; } +.badge-featured { background: var(--phet-orange); color: #fff; } +.badge-html5 { background: var(--phet-blue); color: #fff; } .badge-version { background: var(--c-bg-alt); color: var(--c-muted); } /* Filter bar */ .filter-bar { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - align-items: flex-end; + display: flex; flex-wrap: wrap; + gap: 0.75rem; align-items: flex-end; background: var(--c-bg-alt); - padding: 0.85rem 1rem; + padding: 1rem 1.25rem; border-radius: var(--radius); - margin-bottom: 1.25rem; + border-left: 4px solid var(--phet-magenta); + margin-bottom: 1.5rem; } .filter-bar label { - display: flex; - flex-direction: column; - font-size: 0.8rem; - color: var(--c-muted); - gap: 0.2rem; + display: flex; flex-direction: column; + font-size: 0.78rem; color: var(--c-muted); + gap: 0.2rem; text-transform: uppercase; letter-spacing: 0.04em; } .filter-bar select { padding: 0.4rem 0.6rem; - border: 1px solid var(--c-border); + border: 1px solid var(--c-border-strong); border-radius: var(--radius); background: #fff; min-width: 160px; + font-size: 0.85rem; } -.search-form-wide { - display: flex; - gap: 0.5rem; - margin: 1rem 0 2rem; +.search-form-wide { display: flex; gap: 0.5rem; margin: 1rem 0 2rem; } +.search-form-wide input { + flex: 1; + padding: 0.6rem 0.8rem; + border: 1px solid var(--c-border-strong); + border-radius: var(--radius); } -.search-form-wide input { flex: 1; padding: 0.6rem 0.8rem; border: 1px solid var(--c-border); border-radius: var(--radius); } /* Pager */ .pager { display: flex; gap: 1rem; align-items: center; justify-content: center; margin: 2rem 0; } .pager-status { color: var(--c-muted); font-size: 0.9rem; } +.pager a { + color: var(--phet-blue); font-weight: 700; + text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.05em; +} + +/* === Browse-by-subject grouping (real PhET) === */ +.browse-hero { + background: + linear-gradient(180deg, rgba(180,225,245,0.0) 0%, rgba(180,225,245,0.0) 100%), + url('../images/ui/banner-underwater.png') center/cover no-repeat, + linear-gradient(180deg, #b3e0f5 0%, #cdedf7 100%); + padding: 2.5rem 1.25rem; + text-align: center; + margin-bottom: 0; + position: relative; + overflow: hidden; + min-height: 140px; + display: flex; align-items: center; justify-content: center; +} +.browse-hero h1 { + margin: 0; + font-size: 2.2rem; + color: var(--c-text); + font-weight: 700; + text-shadow: 0 1px 4px rgba(255,255,255,0.6); + position: relative; + z-index: 1; +} +.browse-subtabs { + display: flex; justify-content: center; gap: 2.5rem; + background: #fff; + border-bottom: 1px solid var(--c-border); + padding: 0.75rem 1.25rem; + margin-bottom: 2rem; +} +.browse-subtab { + font-size: 0.95rem; + color: var(--c-muted); + font-weight: 700; + text-transform: capitalize; + position: relative; padding: 0.3rem 0; + text-decoration: none; +} +.browse-subtab.active { color: var(--c-text); } +.browse-subtab.active::after { + content: ""; position: absolute; + left: 0; right: 0; bottom: -0.8rem; + height: 3px; background: var(--phet-magenta); +} + +/* === Filter sidebar layout === */ +.browse-layout { + display: grid; + grid-template-columns: 240px 1fr; + gap: 2rem; + max-width: var(--max-width); margin: 0 auto; + padding: 1.5rem 1.25rem; +} +@media (max-width: 800px) { .browse-layout { grid-template-columns: 1fr; } } + +.filter-sidebar { + font-size: 0.85rem; +} +.filter-group { margin-bottom: 1rem; border-bottom: 1px solid var(--c-border); padding-bottom: 0.6rem; } +.filter-group > summary { + cursor: pointer; + font-weight: 700; + text-transform: uppercase; + font-size: 0.78rem; + letter-spacing: 0.05em; + color: var(--c-text); + padding: 0.5rem 0; + list-style: none; + outline: none; + display: flex; align-items: center; justify-content: space-between; +} +.filter-group > summary::-webkit-details-marker { display: none; } +.filter-group > summary::after { + content: "▾"; + color: var(--c-muted); + font-size: 0.7rem; + transition: transform 0.2s; +} +.filter-group[open] > summary::after { transform: rotate(180deg); } +.filter-group ul { + list-style: none; padding: 0.3rem 0 0 0.15rem; + margin: 0; +} +.filter-group li { margin: 0.25rem 0; } +.filter-group label { + display: flex; align-items: flex-start; gap: 0.45rem; + font-size: 0.83rem; + color: var(--c-text); + cursor: pointer; + padding: 0.15rem 0; +} +.filter-group label:hover { color: var(--phet-magenta); } +.filter-group input[type=checkbox], +.filter-group input[type=radio] { + margin-top: 0.18rem; + accent-color: var(--phet-magenta); +} +.filter-group .sub-group { + margin-left: 0.9rem; + padding-left: 0.5rem; + border-left: 2px solid var(--c-border); + margin-bottom: 0.35rem; +} +.filter-group .sub-group-title { + font-weight: 700; + font-size: 0.78rem; + color: var(--phet-blue); + margin: 0.35rem 0 0.2rem; +} +.filter-clear { + display: block; + margin: 1rem 0; + padding: 0.45rem 0.9rem; + border: 1px solid var(--c-border-strong); + background: #fff; color: var(--c-text); + text-align: center; + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + border-radius: 3px; + cursor: pointer; + width: 100%; +} +.filter-clear:hover { border-color: var(--phet-magenta); color: var(--phet-magenta); text-decoration: none; } -/* Sim detail */ +.browse-results {} +.browse-results-head { + display: flex; align-items: center; gap: 0.75rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--c-border); + padding-bottom: 0.75rem; +} +.results-count { + font-size: 1.05rem; font-weight: 700; + color: var(--c-text); +} +.results-active-chips { + display: flex; gap: 0.4rem; flex-wrap: wrap; + margin-left: 0.75rem; +} +.results-chip { + display: inline-flex; align-items: center; gap: 0.3rem; + background: var(--phet-blue); + color: #fff; + font-size: 0.72rem; + padding: 0.15rem 0.45rem; + border-radius: 2px; + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 700; +} +.results-chip a { + color: #fff; opacity: 0.85; + margin-left: 0.15rem; +} +.results-chip a:hover { opacity: 1; text-decoration: none; } +.results-sort { + margin-left: auto; + display: flex; align-items: center; gap: 0.4rem; +} +.results-sort label { font-size: 0.78rem; color: var(--c-muted); text-transform: uppercase; } +.results-sort select { + padding: 0.3rem 0.6rem; + border: 1px solid var(--c-border-strong); + border-radius: 3px; + background: #fff; + font-size: 0.85rem; +} + +.subject-section { margin: 0 0 2.5rem; padding: 0 1.25rem; } +.subject-section-head { + display: flex; align-items: center; gap: 0.5rem; + margin-bottom: 1rem; + padding-bottom: 0.4rem; + border-bottom: 1px solid var(--c-border); +} +.subject-section-head h2 { + margin: 0; font-size: 1.1rem; font-weight: 700; color: var(--c-text); +} +.subject-section-head .more { + margin-left: auto; + color: var(--phet-magenta); + font-size: 0.85rem; + font-weight: 700; + text-decoration: none; +} +.subject-section-head .more:hover { text-decoration: underline; } + +/* Language dots under sim cards (real PhET shows colored squares) */ +.lang-dots { + display: flex; gap: 2px; margin-top: 4px; + flex-wrap: wrap; +} +.lang-dot { + width: 10px; height: 10px; + background: var(--phet-magenta); + border-radius: 1px; + display: inline-block; +} +.lang-dot.muted { background: #d8dde3; } + +/* === Sim detail === */ .sim-detail-head { margin-bottom: 1.5rem; } -.sim-detail-head h1 { margin: 0 0 0.5rem; font-size: 2rem; } -.sim-detail-tagline { color: var(--c-muted); font-size: 1.05rem; margin: 0 0 0.75rem; } +.sim-detail-head h1 { margin: 0 0 0.5rem; font-size: 1.9rem; color: var(--phet-orange); font-weight: normal; } +.sim-detail-tagline { color: var(--c-muted); font-size: 1.02rem; margin: 0 0 0.75rem; } .badge-row { list-style: none; padding: 0; margin: 0; display: flex; gap: 0.25rem; flex-wrap: wrap; } .sim-detail-body { display: grid; grid-template-columns: 2.4fr 1fr; gap: 2rem; } @media (max-width: 800px) { .sim-detail-body { grid-template-columns: 1fr; } } .sim-detail-frame { - background: linear-gradient(135deg, #0079bf 0%, #6cba5c 100%); - aspect-ratio: 16 / 10; - border-radius: var(--radius); - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - gap: 1rem; - color: #fff; + position: relative; + aspect-ratio: 3 / 2; + background: #f4f6f8; + box-shadow: var(--shadow-card-hover); + display: flex; align-items: center; justify-content: center; + flex-direction: column; gap: 1rem; margin-bottom: 1.5rem; + overflow: hidden; +} +.sim-detail-frame img { + width: 100%; height: 100%; + object-fit: cover; display: block; +} +.sim-detail-frame-placeholder { + width: 100%; height: 100%; + background: linear-gradient(135deg, var(--phet-navy) 0%, var(--phet-blue) 100%); + color: #fff; + display: flex; align-items: center; justify-content: center; + font-size: 1.4rem; font-weight: 700; + text-align: center; padding: 1rem; +} +.sim-detail-play-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,0.0); + display: flex; align-items: center; justify-content: center; + transition: background 0.2s; + cursor: pointer; + text-decoration: none; +} +.sim-detail-play-overlay:hover { background: rgba(0,0,0,0.4); text-decoration: none; } +.sim-detail-play-overlay::before { + content: "▶ PLAY"; + background: var(--phet-magenta); + color: #fff; + padding: 0.85rem 1.8rem; + font-weight: 800; + letter-spacing: 0.08em; + border-radius: var(--radius); + font-size: 1rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + opacity: 0; + transition: opacity 0.2s; +} +.sim-detail-play-overlay:hover::before { opacity: 1; } +.sim-detail-section { margin: 1.75rem 0; } +.sim-detail-section h2 { + font-size: 1.05rem; + color: var(--phet-blue); + font-weight: 700; + border-bottom: 2px solid var(--phet-blue); + padding-bottom: 0.3rem; + margin: 0 0 0.75rem; } -.sim-detail-frame-label { font-size: 1.4rem; font-weight: 700; } -.sim-detail-section { margin: 1.5rem 0; } -.sim-detail-section h2 { font-size: 1.2rem; margin: 0 0 0.5rem; } .topic-list, .activity-list { padding-left: 1.5rem; } .activity-list li { margin-bottom: 0.5rem; } .activity-meta { color: var(--c-muted); font-size: 0.85rem; } -.side-card { +/* Tab nav on detail page */ +.detail-tabs { + display: flex; gap: 2rem; + border-bottom: 1px solid var(--c-border); + margin: 1.5rem 0; + padding: 0; +} +.detail-tab { + font-size: 0.95rem; + color: var(--c-muted); + font-weight: 700; + padding: 0.5rem 0; + text-decoration: none; + position: relative; + cursor: pointer; + background: transparent; + border: 0; +} +.detail-tab.active { color: var(--c-text); } +.detail-tab.active::after { + content: ""; position: absolute; + left: 0; right: 0; bottom: -1px; + height: 3px; background: var(--phet-magenta); +} +.detail-tab:hover { color: var(--phet-magenta); text-decoration: none; } + +.share-row { + display: flex; gap: 0.5rem; + margin: 0.75rem 0 1rem; +} +.share-row a { + width: 28px; height: 28px; + display: flex; align-items: center; justify-content: center; background: var(--c-bg-alt); - border-radius: var(--radius); + color: var(--phet-navy); + border-radius: 3px; +} +.share-row a:hover { background: var(--phet-magenta); color: #fff; } + +.supported-by { + background: #fff; + border: 1px solid var(--c-border); + padding: 1.25rem 1rem; + text-align: center; + margin-bottom: 1rem; +} +.supported-by-label { + font-size: 0.78rem; + color: var(--c-muted); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.75rem; + display: block; +} +.supported-by-logo { + font-family: Georgia, serif; + font-style: italic; + font-size: 1.3rem; + color: var(--phet-navy); + font-weight: 700; +} +.supported-by-foot { + font-size: 0.78rem; + color: var(--c-muted); + margin-top: 0.75rem; + line-height: 1.4; +} + +/* Explore More + DONATE bottom CTA */ +.explore-more { + background: #fde4ee; + padding: 1.5rem 2rem; + margin: 2.5rem 0 0; + text-align: center; + border-radius: 4px; +} +.explore-more h3 { + font-size: 0.78rem; + color: var(--phet-magenta); + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 700; + margin: 0 0 0.4rem; +} +.explore-more-links { font-size: 1.05rem; font-weight: 700; color: var(--c-text); } +.explore-more-links a { color: var(--c-text); } +.explore-more-links a:hover { color: var(--phet-magenta); text-decoration: none; } +.donate-cta { + background: var(--phet-magenta); color: #fff; + padding: 0.85rem 2.5rem; + font-weight: 700; font-size: 0.9rem; + border-radius: 4px; + text-transform: uppercase; letter-spacing: 0.05em; + display: inline-block; + margin-top: 1rem; +} +.donate-cta:hover { background: var(--phet-magenta-dark); color: #fff; text-decoration: none; } + +.side-card { + background: #fff; + border: 1px solid var(--c-border); + border-top: 3px solid var(--phet-magenta); + border-radius: 0; padding: 1rem 1.2rem; margin-bottom: 1rem; } -.side-card h3 { margin: 0 0 0.5rem; font-size: 0.95rem; } +.side-card h3 { + margin: 0 0 0.6rem; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--phet-navy); + font-weight: 700; +} .side-card ul { margin: 0; padding: 0; list-style: none; } -.side-card li { padding: 0.2rem 0; font-size: 0.9rem; border-bottom: 1px solid #e1e6eb; } +.side-card li { padding: 0.2rem 0; font-size: 0.88rem; border-bottom: 1px solid #e1e6eb; } .side-card li:last-child { border: 0; } .lang-list { max-height: 220px; overflow-y: auto; } .save-status { color: var(--c-success); font-size: 0.85rem; margin: 0.5rem 0 0; } /* Languages */ .language-grid { - list-style: none; - padding: 0; + list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75rem; @@ -296,28 +994,28 @@ a:hover { text-decoration: underline; } border-radius: var(--radius); } .language-card a { - display: flex; - flex-direction: column; + display: flex; flex-direction: column; padding: 1rem 1.2rem; color: var(--c-text); text-decoration: none; } +.language-card a:hover { background: var(--c-bg-alt); } .language-card.rtl a { direction: rtl; text-align: right; } -.language-name { font-weight: 700; font-size: 1rem; } -.language-native { color: var(--c-muted); font-size: 0.88rem; } -.language-count { font-size: 0.8rem; color: var(--c-primary); margin-top: 0.25rem; } +.language-name { font-weight: 700; font-size: 0.95rem; color: var(--phet-blue); } +.language-native { color: var(--c-muted); font-size: 0.85rem; } +.language-count { font-size: 0.78rem; color: var(--phet-magenta); margin-top: 0.25rem; font-weight: 700; } /* Teachers */ .teachers-hero { - background: linear-gradient(135deg, #fff4e6 0%, #ffe5cc 100%); - padding: 2rem; + background: var(--c-bg-alt); + border-left: 5px solid var(--phet-orange); + padding: 1.75rem 2rem; border-radius: var(--radius); margin-bottom: 2rem; } -.teachers-hero h1 { margin: 0 0 0.5rem; } +.teachers-hero h1 { margin: 0 0 0.5rem; color: var(--phet-orange); font-weight: normal; } .activity-grid { - list-style: none; - padding: 0; + list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; @@ -325,31 +1023,31 @@ a:hover { text-decoration: underline; } .activity-card { background: #fff; border: 1px solid var(--c-border); - border-radius: var(--radius); + border-top: 3px solid var(--phet-orange); padding: 1.1rem 1.2rem; } -.activity-card h3 { margin: 0 0 0.4rem; font-size: 1.05rem; } +.activity-card h3 { margin: 0 0 0.4rem; font-size: 1rem; } +.activity-card h3 a { color: var(--phet-blue); } .activity-author { color: var(--c-muted); font-size: 0.82rem; margin-top: 0.5rem; } .activity-detail .activity-section { margin: 1.5rem 0; } -.activity-detail h1 { margin: 0 0 0.5rem; } +.activity-detail h1 { margin: 0 0 0.5rem; color: var(--phet-orange); font-weight: normal; } /* Category */ .category-header { background: var(--c-bg-alt); - border-left: 5px solid var(--tile-color, var(--c-primary)); + border-left: 5px solid var(--tile-color, var(--phet-blue)); padding: 1.5rem; border-radius: var(--radius); margin-bottom: 1.5rem; } -.category-header h1 { margin: 0 0 0.25rem; } +.category-header h1 { margin: 0 0 0.25rem; color: var(--phet-orange); font-weight: normal; } .category-count { color: var(--c-muted); margin: 0.5rem 0 0; } /* About */ .prose { max-width: 720px; } -.prose h2 { margin-top: 2rem; } +.prose h2 { margin-top: 2rem; color: var(--phet-blue); } .stats-grid { - list-style: none; - padding: 0; + list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1rem; @@ -360,8 +1058,9 @@ a:hover { text-decoration: underline; } padding: 1.2rem; border-radius: var(--radius); text-align: center; + border-top: 3px solid var(--phet-magenta); } -.stat-card strong { display: block; font-size: 1.8rem; color: var(--c-primary); } +.stat-card strong { display: block; font-size: 1.8rem; color: var(--phet-blue); } .stat-card span { color: var(--c-muted); font-size: 0.85rem; } /* Account */ @@ -369,34 +1068,37 @@ a:hover { text-decoration: underline; } .saved-item { background: #fff; border: 1px solid var(--c-border); - border-radius: var(--radius); + border-left: 3px solid var(--phet-magenta); padding: 1rem 1.25rem; margin-bottom: 0.75rem; } .saved-item h3 { margin: 0 0 0.25rem; font-size: 1.05rem; } +.saved-item h3 a { color: var(--phet-blue); } .saved-desc { color: var(--c-muted); margin: 0 0 0.5rem; font-size: 0.9rem; } .saved-notes { background: #fff8e1; padding: 0.5rem 0.75rem; border-radius: 4px; font-size: 0.88rem; } .saved-meta { color: var(--c-muted); font-size: 0.8rem; margin: 0.4rem 0 0; } /* Auth */ .auth-card { - max-width: 420px; - margin: 2rem auto; + max-width: 420px; margin: 2rem auto; background: #fff; border: 1px solid var(--c-border); - border-radius: var(--radius); + border-top: 4px solid var(--phet-magenta); padding: 2rem; - box-shadow: var(--shadow); + box-shadow: 0 2px 8px rgba(0,0,0,0.05); } -.auth-card h1 { margin: 0 0 0.5rem; font-size: 1.5rem; } +.auth-card h1 { margin: 0 0 0.5rem; font-size: 1.4rem; color: var(--phet-orange); font-weight: normal; } .auth-sub { color: var(--c-muted); margin: 0 0 1.5rem; font-size: 0.9rem; } -.auth-card label { display: block; margin-bottom: 0.85rem; font-size: 0.88rem; color: var(--c-muted); } +.auth-card label { + display: block; margin-bottom: 0.85rem; + font-size: 0.78rem; color: var(--c-muted); + text-transform: uppercase; letter-spacing: 0.05em; +} .auth-card input { - display: block; - width: 100%; + display: block; width: 100%; margin-top: 0.2rem; padding: 0.55rem 0.7rem; - border: 1px solid var(--c-border); + border: 1px solid var(--c-border-strong); border-radius: var(--radius); font-size: 0.95rem; } @@ -407,27 +1109,101 @@ a:hover { text-decoration: underline; } .error-page ul { list-style: none; padding: 0; } .error-page li { display: inline-block; margin: 0 0.5rem; } -/* Footer */ -.footer { - background: #1f2933; - color: #d4d8de; - padding: 2.5rem 0 1.5rem; - margin-top: 3rem; +/* === Footer (white top + grey sponsor band, real PhET look) === */ +.footer-top { + background: #fff; + padding: 2.5rem 1.25rem 1.5rem; + border-top: 1px solid var(--c-border); } -.footer-inner { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 1.5rem; -} -.footer h4 { color: #fff; margin: 0 0 0.6rem; font-size: 0.95rem; } -.footer ul { list-style: none; padding: 0; margin: 0; } -.footer li { margin-bottom: 0.3rem; } -.footer a { color: #d4d8de; font-size: 0.88rem; } -.footer a:hover { color: #fff; } +.footer-social { + display: flex; justify-content: center; gap: 1.25rem; + margin-bottom: 1.25rem; +} +.footer-social a { + width: 22px; height: 22px; + display: flex; align-items: center; justify-content: center; + color: var(--c-text); +} +.footer-social a:hover { color: var(--phet-magenta); } +.footer-cols { + max-width: 720px; margin: 0 auto; + display: grid; grid-template-columns: repeat(4, 1fr); + gap: 1rem; text-align: center; +} +@media (max-width: 600px) { .footer-cols { grid-template-columns: repeat(2, 1fr); } } +.footer-cols ul { list-style: none; padding: 0; margin: 0; } +.footer-cols li { margin: 0.2rem 0; } +.footer-cols a { + color: var(--c-muted); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.footer-cols a:hover { color: var(--phet-magenta); text-decoration: none; } + +.footer-lang { + text-align: center; margin: 1.5rem 0; +} +.footer-lang select { + padding: 0.35rem 1.5rem 0.35rem 0.6rem; + border: 1px solid var(--c-border-strong); + border-radius: 3px; + background: #fff; + font-size: 0.85rem; + color: var(--c-text); + min-width: 180px; +} +.footer-apps { + display: flex; justify-content: center; gap: 0.75rem; + margin: 1.25rem 0; + flex-wrap: wrap; +} +.footer-apps img { height: 36px; } +.footer-apps-link { + display: block; text-align: center; + font-size: 0.75rem; color: var(--c-text); + text-transform: uppercase; letter-spacing: 0.05em; + margin: 0.75rem 0; + font-weight: 700; +} +.footer-apps-link:hover { color: var(--phet-magenta); } + +/* Real sponsor logos strip (light grey) */ +.sponsor-strip { + background: #f0f0f0; + padding: 2rem 1.25rem; + margin: 0; + border-top: 1px solid var(--c-border); +} +.sponsor-inner { + max-width: var(--max-width); margin: 0 auto; + display: flex; align-items: center; justify-content: center; + flex-wrap: wrap; gap: 1.5rem 2.5rem; +} +.sponsor-inner img { + max-height: 50px; + width: auto; + filter: grayscale(100%); + opacity: 0.8; + transition: opacity 0.2s, filter 0.2s; +} +.sponsor-inner img:hover { filter: none; opacity: 1; } +.sponsor-phet-mark { + display: flex; align-items: center; gap: 0.75rem; + font-size: 0.75rem; color: var(--c-muted); + border-left: 1px solid var(--c-border-strong); + padding-left: 1.5rem; + margin-left: 1rem; +} +.sponsor-phet-mark img { height: 28px; filter: none; opacity: 1; } +.sponsor-phet-mark span { line-height: 1.3; } + +/* Final fineprint footer (very small bottom strip) */ .footer-fineprint { - border-top: 1px solid #2d3742; - padding-top: 1rem; - margin-top: 1.5rem; - color: #8c95a1; - font-size: 0.82rem; + background: #f0f0f0; + border-top: 1px solid var(--c-border); + padding: 0.85rem 1.25rem; + color: var(--c-muted); + font-size: 0.72rem; + text-align: center; } diff --git a/sites/phet_simulations/templates/_sim_card.html b/sites/phet_simulations/templates/_sim_card.html index 6e061aadc..07db58564 100644 --- a/sites/phet_simulations/templates/_sim_card.html +++ b/sites/phet_simulations/templates/_sim_card.html @@ -1,18 +1,24 @@
- {{ sim.title }} + {% if sim.slug in available_thumbnails %} + {{ sim.title }} + {% else %} + {{ sim.title }} + {% endif %} + {% if sim.is_html5 %}H5{% endif %}

{{ sim.title }}

-

{{ sim.short_description }}

-
    - {% for subj_slug in sim.subjects() %} -
  • {{ subj_slug.replace('-', ' ').title() }}
  • - {% endfor %} -
- {% if sim.is_new %}New{% endif %} - {% if sim.is_featured %}Featured{% endif %} + {% set lang_count = sim.languages()|length %} + {% if lang_count %} +
+ {% for i in range(8) %} + {% if i < lang_count %}{% else %}{% endif %} + {% endfor %} +
+ {% endif %}
diff --git a/sites/phet_simulations/templates/base.html b/sites/phet_simulations/templates/base.html index 7babf18d1..24094cf34 100644 --- a/sites/phet_simulations/templates/base.html +++ b/sites/phet_simulations/templates/base.html @@ -9,34 +9,95 @@
-
+
+ Educators: To receive PhET's monthly newsletter, + register for a free educator account. + +
+ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
@@ -47,53 +108,102 @@ {% endif %} {% endwith %} -
+{% block hero %}{% endblock %} +{% block stats %}{% endblock %} +{% block full_bleed_top %}{% endblock %} + +
{% block content %}{% endblock %}
- + + + Get Apps for Schools +
+ + + + diff --git a/sites/phet_simulations/templates/index.html b/sites/phet_simulations/templates/index.html index ad38d06a1..2b374d69e 100644 --- a/sites/phet_simulations/templates/index.html +++ b/sites/phet_simulations/templates/index.html @@ -1,53 +1,115 @@ {% extends "base.html" %} -{% block content %} -
-
-

Free interactive math & science simulations

-

Research-based simulations from the University of Colorado Boulder. - Browse {{ total_simulations }} simulations across physics, chemistry, - math, biology, and earth science — available in {{ total_languages }} languages.

-

- Browse all simulations - For teachers -

+{% set subjects_ordered = [ + ('physics', 'Physics'), + ('math', 'Math & Statistics'), + ('chemistry', 'Chemistry'), + ('earth-science', 'Earth & Space'), + ('biology', 'Biology'), +] %} +{% set subj_icon = { + 'physics': 'physics', + 'math': 'math-and-statistics', + 'chemistry': 'chemistry', + 'earth-science': 'earth-and-space', + 'biology': 'biology', +} %} + +{% block hero %} +
+
+
+

Interactive Simulations
for Science and Math

+ Explore our sims +
+{% endblock %} -
- {% for subject in primary_subjects %} - - - {{ subject.name }} - {{ subject.description }} +{% block stats %} +
+ Over 1.8 billion simulations delivered +
+{% endblock %} + +{% block main_class %}container home-main{% endblock %} + +{% block content %} + +
+ {% for slug, name in subjects_ordered %} + + + {{ name }} {% endfor %}
+{% endblock %} + +{% block full_bleed_top %}{% endblock %} -
-

Featured simulations

-
- {% for sim in featured %} - {% include "_sim_card.html" %} - {% endfor %} +{% block full_bleed_bottom %} +
+
+
+

Teaching Resources,
Activities, and Community

+

Teachers have access to simulations specific to grade and video content, + resources for teaching with simulations, and activities created by our + teacher community.

+ Register now +
-
-

New & recently updated

-
- {% for sim in new_sims %} - {% include "_sim_card.html" %} - {% endfor %} +
+
+ Interact. Discover. Learn.
+
+
+ {{ total_simulations }} + interactive simulations +
+
+
+ {{ total_languages }} + language translations +
+
+
+ {{ total_activities }} + teacher-submitted lessons +
+
+
+

+ Founded in 2002 by Nobel Laureate Carl Wieman, the PhET Interactive Simulations + project at the University of Colorado Boulder creates free interactive math and + science simulations. PhET sims are based on extensive + research + and engage students through an intuitive, game-like environment where students + learn through exploration and discovery. +

-
-

Most played this month

-
- {% for sim in most_played %} - {% include "_sim_card.html" %} - {% endfor %} +
+
+
+

Shouldn't All Students
Experience STEM?

+

Learn how we are tackling challenges in STEM education, software development, + and assistive technology.

+ Explore accessible sims +
+ +
+
PhET is supported in part by
+ +

+ and other supporters, including educators like you. +

+ +
{% endblock %} diff --git a/sites/phet_simulations/templates/simulation_detail.html b/sites/phet_simulations/templates/simulation_detail.html index 35f42841d..b440e894a 100644 --- a/sites/phet_simulations/templates/simulation_detail.html +++ b/sites/phet_simulations/templates/simulation_detail.html @@ -8,41 +8,68 @@
-
-

{{ sim.title }}

-

{{ sim.short_description }}

-
    - {% if sim.is_html5 %}
  • HTML5
  • {% endif %} - {% if sim.is_new %}
  • New
  • {% endif %} - {% if sim.is_featured %}{% endif %} -
  • v{{ sim.version }}
  • -
-
-
+
- {{ sim.title }} - + {% if sim.slug in available_thumbnails %} + {{ sim.title }} screenshot + {% else %} +
{{ sim.title }}
+ {% endif %} +
-
+
+

{{ sim.title }}

+
    + {% if sim.is_html5 %}
  • HTML5
  • {% endif %} + {% if sim.is_new %}
  • New
  • {% endif %} + {% if sim.is_featured %}{% endif %} +
  • v{{ sim.version }}
  • +
+ +
+ + + +

About this simulation

{{ sim.overview }}

+

{{ sim.short_description }}

-

Learning goals

+

Topics

    - {% for topic in sim.topics() %} -
  • {{ topic|capitalize }}
  • - {% endfor %} + {% for topic in sim.topics() %}
  • {{ topic|capitalize }}
  • {% endfor %}
- {% if activities %}
-

Teacher-submitted activities ({{ activities|length }})

+

Sample Learning Goals

+
    +
  • Use the {{ sim.title.lower() }} simulation to model the underlying concepts.
  • +
  • Identify the variables and determine the relationships among them.
  • +
  • Predict how changes to one parameter affect the rest of the system.
  • +
+
+ + {% if activities %} +
+

Teaching Resources — submitted activities ({{ activities|length }})

    {% for activity in activities %}
  • @@ -54,9 +81,25 @@

    Teacher-submitted activities ({{ activities|length }})

{% endif %} +
+

Translations ({{ languages|length }})

+
+ {% for l in languages %} + {{ l.name }} + {% endfor %} +
+
+ +
+

System Requirements & Credits

+

HTML5 simulations can run on Chromebooks, iPads, iPhones, PCs, Mac, and Linux systems. + Inclusive features added to HTML5 sims only; some features have platform limitations.

+

Version: {{ sim.version }} · Released {{ sim.release_date }}

+
+ {% if related %}
-

Related simulations

+

Related Sims

{% for sim in related %} {% include "_sim_card.html" %} @@ -67,6 +110,12 @@

Related simulations

+ +
+

Explore More

+ +

+ PhET is a non-profit committed to providing high quality STEM resources for every classroom. +

+ +
{% endblock %} diff --git a/sites/phet_simulations/templates/simulations.html b/sites/phet_simulations/templates/simulations.html index 42f2f2aef..693e54d86 100644 --- a/sites/phet_simulations/templates/simulations.html +++ b/sites/phet_simulations/templates/simulations.html @@ -1,68 +1,206 @@ {% extends "base.html" %} -{% block title %}All Simulations · {{ site_title }}{% endblock %} +{% block title %}Simulations · {{ site_title }}{% endblock %} + +{% block hero %} +
+

Simulations

+
+{% endblock %} + +{% block stats %} + +{% endblock %} + +{% block main_class %}{% endblock %} + {% block content %} -

All Simulations

-

{{ total }} simulation{{ '' if total == 1 else 's' }} matching your filters.

- -
- - - - - - Reset -
- -{% if sims %} -
- {% for sim in sims %} - {% include "_sim_card.html" %} - {% endfor %} -
- {% if total_pages > 1 %} - - {% endif %} +
+
+ {% else %} -

No simulations match the selected filters. Try clearing one of the filters above.

+ {# === Browse mode: grouped by subject (real PhET default) === #} +
+ {% for slug, name in [('physics', 'Physics'), ('math', 'Math & Statistics'), + ('chemistry', 'Chemistry'), ('earth-science', 'Earth & Space'), + ('biology', 'Biology')] %} + {% set bucket = sims_by_subject.get(slug, []) %} + {% if bucket %} +
+
+

{{ name }}

+ View all » +
+
+ {% for sim in bucket %} + {% include "_sim_card.html" %} + {% endfor %} +
+
+ {% endif %} + {% endfor %} +
{% endif %} + {% endblock %} From 749726f7a70a30c11bb74e76d74452f9db82a95d Mon Sep 17 00:00:00 2001 From: Zhongyang Li Date: Thu, 2 Jul 2026 00:35:11 +0000 Subject: [PATCH 03/10] fix(phet_simulations): address PR #29 review feedback Addresses all points from MufanQiu's review: 1. Homepage now renders simulation grids (BLOCKER). index.html displays the route's featured / new_sims / most_played context in three subject-section rows (20 sim cards), so task --41 is solvable from /. 2. 'New' release filter is functional (MAJOR). simulations() now reads release=new (is_new) and release=updated (released >= 2024-01-01); the sidebar checkboxes persist state and the active-filter chip, sort, and pagination links carry the param. Task --21 is solvable via UI (answer: 7 New sims, all released 2025). 3. No DB mutation on GET (MAJOR). Removed play_count increment in simulation_detail and download_count increment in activity_detail; counts are now fixed seed data, so tasks --6/--31/--41 have deterministic answers. 4. Seed/task disambiguation (MINOR): - Activities carry explicit pairwise-distinct download counts; unique most-downloaded = Net Force Investigation (8742), paired with Forces and Motion: Basics (task --12). - Sim versions derived deterministically from seed constants (47 distinct versions; Wave Interference = 1.5.2, task --28). - Tasks --6, --24, --28, --31 reworded with tighter constraints so each has exactly one valid answer (verified programmatically). Seed DB rebuilt; reset cycle remains byte-idempotent (md5 48ca438b8ac6dab37a6503a4e5574503). 66/66 routes return 200. New phet_simulations.tar.gz must be re-uploaded to the HF dataset and .assets-revision repinned to the HF merge SHA (separate commit). --- sites/phet_simulations/app.py | 57 ++++++++++++------- sites/phet_simulations/tasks.jsonl | 8 +-- sites/phet_simulations/templates/index.html | 36 ++++++++++++ .../templates/simulations.html | 26 ++++++--- 4 files changed, 93 insertions(+), 34 deletions(-) diff --git a/sites/phet_simulations/app.py b/sites/phet_simulations/app.py index 34bf42f40..107acbd0a 100644 --- a/sites/phet_simulations/app.py +++ b/sites/phet_simulations/app.py @@ -256,6 +256,7 @@ def simulations(): subject = request.args.get("subject", "").strip() grade = request.args.get("grade", "").strip() language = request.args.get("language", "").strip() + release = request.args.get("release", "").strip() sort = request.args.get("sort", "title") view = request.args.get("view", "filter").strip() page = max(int(request.args.get("page", 1)), 1) @@ -268,6 +269,11 @@ def simulations(): query = query.filter(Simulation.grades_json.like(f'%"{grade}"%')) if language: query = query.filter(Simulation.languages_json.like(f'%"{language}"%')) + if release == "new": + query = query.filter_by(is_new=True) + elif release == "updated": + # "Recently updated" in this snapshot = released in 2024 or later. + query = query.filter(Simulation.release_date >= date(2024, 1, 1)) if sort == "newest": query = query.order_by(Simulation.release_date.desc()) @@ -280,7 +286,9 @@ def simulations(): sims = query.offset((page - 1) * per_page).limit(per_page).all() total_pages = max((total + per_page - 1) // per_page, 1) - any_filter_active = bool(subject or grade or language or sort != "title") + any_filter_active = bool( + subject or grade or language or release or sort != "title" + ) view_tab = view if view in ("browse", "filter", "customize") else "filter" sims_by_subject = {} @@ -304,6 +312,7 @@ def simulations(): subject=subject, grade=grade, language=language, + release=release, sort=sort, view_tab=view_tab, languages=Language.query.order_by(Language.name).all(), @@ -327,9 +336,9 @@ def simulations_by_subject(slug): @app.route("/simulation/") def simulation_detail(slug): + # Read-only: GET must never mutate the DB, or task answers that + # reference play counts drift between visits. sim = Simulation.query.filter_by(slug=slug).first_or_404() - sim.play_count = (sim.play_count or 0) + 1 - db.session.commit() subjects_full = [ s for s in Subject.query.filter(Subject.slug.in_(sim.subjects())).all() @@ -438,9 +447,8 @@ def activities(): @app.route("/teachers/activity/") def activity_detail(activity_id): + # Read-only: download counts are fixed seed data (see ACTIVITIES_SEED). activity = Activity.query.get_or_404(activity_id) - activity.download_count = (activity.download_count or 0) + 1 - db.session.commit() return render_template("activity_detail.html", activity=activity) @@ -1059,61 +1067,64 @@ def _slug(title): ] +# Download counts are explicit and pairwise-distinct so "most downloaded" +# has exactly one answer (Net Force Investigation, 8742). ACTIVITIES_SEED = [ + # (sim_title, title, author, grade, duration_min, downloads, description) ("Forces and Motion: Basics", "Net Force Investigation", - "Dr. Trish Loeblein", "high", 50, + "Dr. Trish Loeblein", "high", 50, 8742, "Students predict, observe, and explain the motion of objects with " "balanced and unbalanced forces using friction and applied force."), ("Build an Atom", "Atomic Structure Lab", - "Emily Moore", "middle", 45, + "Emily Moore", "middle", 45, 7310, "Build atoms of the first 10 elements, identify subatomic particles, " "and explore how protons determine the element."), ("Balancing Chemical Equations", "Coefficient Practice", - "Yuen-Ying Carpenter", "high", 60, + "Yuen-Ying Carpenter", "high", 60, 6485, "Practice balancing combustion, synthesis, and decomposition reactions " "using the conservation of mass."), ("States of Matter", "Phase Change Inquiry", - "Sam McKagan", "middle", 40, + "Sam McKagan", "middle", 40, 5121, "Investigate the relationship between temperature, kinetic energy, " "and phase transitions for water, neon, oxygen, and argon."), ("Natural Selection", "Evolution of Bunnies", - "Wendy Adams", "high", 55, + "Wendy Adams", "high", 55, 6893, "Model how variation, environment, and selection pressure together " "drive allele frequency change across generations."), ("Gravity and Orbits", "Modeling the Solar System", - "Noah Finkelstein", "middle", 50, + "Noah Finkelstein", "middle", 50, 5764, "Manipulate masses and distances to explore how gravitational force " "shapes planetary orbits in our solar system."), ("pH Scale", "Acids and Bases in the Kitchen", - "Kelly Lancaster", "middle", 40, + "Kelly Lancaster", "middle", 40, 4937, "Predict, measure, and rank common household solutions by pH and " "categorize each as acid, base, or neutral."), ("Plate Tectonics", "Boundary Identification", - "Karina Hensberry", "high", 45, + "Karina Hensberry", "high", 45, 3608, "Use animations to classify convergent, divergent, and transform " "boundaries and connect each to real-world geologic features."), ("Greenhouse Effect", "Climate Modeling Lab", - "Trish Loeblein", "high", 60, + "Trish Loeblein", "high", 60, 5342, "Model how atmospheric composition affects equilibrium temperature " "with and without greenhouse gases."), ("Circuit Construction Kit: DC", "Series and Parallel", - "John De La Cruz", "high", 50, + "John De La Cruz", "high", 50, 6178, "Compare current and voltage in series vs parallel arrangements " "and verify Kirchhoff's laws empirically."), ("Fractions: Intro", "Equivalent Fractions Game", - "Amanda McGarry", "elementary", 30, + "Amanda McGarry", "elementary", 30, 4215, "Use bar models, number lines, and circle models to identify " "equivalent fractions and develop fluency."), ("Energy Skate Park", "Conservation of Energy", - "Karina Hensberry", "high", 55, + "Karina Hensberry", "high", 55, 7026, "Track potential, kinetic, thermal, and total energy as a skater " "moves through changing terrain."), ("Density", "Identify the Mystery Block", - "Emily Moore", "middle", 35, + "Emily Moore", "middle", 35, 3891, "Use mass and volume measurements to identify the material of " "unknown solid blocks and explain buoyancy in water."), ("Graphing Lines", "Slope-Intercept Form", - "Dr. Karina Hensberry", "middle", 40, + "Dr. Karina Hensberry", "middle", 40, 4560, "Investigate how m and b transform the line y = mx + b and apply " "this to real-world rate problems."), ] @@ -1169,6 +1180,9 @@ def seed_simulations(): featured, is_new, runtime, year, month, day, plays) = row slug = _slug(title) languages = ["en"] + list(extra_langs) + # Version derived deterministically from stable seed constants so + # sims carry distinct, reproducible version strings (task --28). + version = f"{1 + plays % 3}.{runtime % 10}.{day % 10}" sim = Simulation( slug=slug, title=title, @@ -1178,6 +1192,7 @@ def seed_simulations(): grades_json=json.dumps(grades), topics_json=json.dumps(topics), languages_json=json.dumps(languages), + version=version, is_html5=True, is_featured=featured, is_new=is_new, @@ -1206,7 +1221,7 @@ def seed_simulations(): def seed_activities(): if Activity.query.count() > 0: return - for sim_title, title, author, grade, duration, desc in ACTIVITIES_SEED: + for sim_title, title, author, grade, duration, downloads, desc in ACTIVITIES_SEED: sim = Simulation.query.filter_by(slug=_slug(sim_title)).first() if not sim: continue @@ -1218,7 +1233,7 @@ def seed_activities(): duration_min=duration, description=desc, file_type="PDF", - download_count=max(duration * 137 % 9000, 350), + download_count=downloads, published_date=date(2024, ((duration % 12) + 1), 15), )) db.session.commit() diff --git a/sites/phet_simulations/tasks.jsonl b/sites/phet_simulations/tasks.jsonl index b60e8ed23..e7b61917a 100644 --- a/sites/phet_simulations/tasks.jsonl +++ b/sites/phet_simulations/tasks.jsonl @@ -4,7 +4,7 @@ {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--3", "ques": "Find the 'Gravity and Orbits' simulation and report what subjects it is tagged with, what grade levels it targets, and when it was released.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--4", "ques": "Find a featured biology simulation about evolution and report its short description and target grade levels.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--5", "ques": "Search the PhET catalog for simulations matching 'DNA' and report how many results are returned.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Find a math simulation suitable for middle school students that covers probability, and report its play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Find the math simulation about probability that is suitable for middle school, high school, and university students, and report its play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--7", "ques": "Open the Translations page on PhET, find Chinese (Simplified), and report how many simulations are available in that language.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--8", "ques": "Find all simulations available in Arabic on PhET, and report whether any of them are tagged as 'New'.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--9", "ques": "List three languages PhET simulations are translated into that use a right-to-left script.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} @@ -22,14 +22,14 @@ {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--21", "ques": "Filter the catalog to show only simulations tagged 'New', and report how many are released in 2025.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--22", "ques": "Find the 'Circuit Construction Kit: DC' simulation and report the full list of languages it is available in.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--23", "ques": "Browse the biology category and report which simulations are appropriate for university-level students.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--24", "ques": "Look up an elementary-school physics simulation about magnets, and report its short description.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--24", "ques": "Look up the elementary-school simulation about magnets that is tagged with both Physics and Earth Science, and report its short description.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--25", "ques": "On the PhET For Teachers page, find a high-school activity longer than 50 minutes and report its title and author.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--26", "ques": "Find a simulation about the greenhouse effect or climate, and report its full overview paragraph.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--27", "ques": "Search for 'genetics' simulations on PhET and report which subjects are covered.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--28", "ques": "Find a simulation about waves and interference, and report what version it is currently at.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--28", "ques": "Open the 'Wave Interference' simulation page and report its current version number.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--29", "ques": "On the PhET catalog, sort simulations by newest first and list the five most recently released.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--30", "ques": "Filter the catalog to show only elementary-school simulations and report how many are available.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--31", "ques": "Find a biology simulation related to predator-prey or population dynamics and report its play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--31", "ques": "Find the biology simulation about predator-prey population cycles that is suitable for middle school students, and report its play count.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--32", "ques": "Browse the PhET catalog and identify a simulation that is tagged as both 'Physics' and 'Math'.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--33", "ques": "Open the Translations page, find Japanese, and click through to view all simulations available in Japanese.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} {"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--34", "ques": "Find the 'Natural Selection' simulation and report its target grade levels and three topics it covers.", "web": "http://localhost:40015/", "upstream_url": "https://phet.colorado.edu/"} diff --git a/sites/phet_simulations/templates/index.html b/sites/phet_simulations/templates/index.html index 2b374d69e..f5a2bcac8 100644 --- a/sites/phet_simulations/templates/index.html +++ b/sites/phet_simulations/templates/index.html @@ -44,6 +44,42 @@

Interactive Simulations
for Science and Math

{% endfor %}
+ +{% if featured %} +
+
+

Featured Simulations

+ View all » +
+
+ {% for sim in featured %}{% include "_sim_card.html" %}{% endfor %} +
+
+{% endif %} + +{% if new_sims %} +
+
+

New Sims

+ View all » +
+
+ {% for sim in new_sims %}{% include "_sim_card.html" %}{% endfor %} +
+
+{% endif %} + +{% if most_played %} +
+
+

Most Played

+ View all » +
+
+ {% for sim in most_played %}{% include "_sim_card.html" %}{% endfor %} +
+
+{% endif %} {% endblock %} {% block full_bleed_top %}{% endblock %} diff --git a/sites/phet_simulations/templates/simulations.html b/sites/phet_simulations/templates/simulations.html index 693e54d86..6babf1628 100644 --- a/sites/phet_simulations/templates/simulations.html +++ b/sites/phet_simulations/templates/simulations.html @@ -87,11 +87,13 @@

Simulations

-
+
Release Type
    -
  • -
  • +
  • +
@@ -131,26 +133,32 @@

Simulations

{% if subject %} {{ subject|upper }} - × + × {% endif %} {% if grade %} {{ grade|upper }} - × + × {% endif %} {% if language %} {{ language|upper }} - × + × + + {% endif %} + {% if release %} + + {{ 'NEW' if release == 'new' else 'RECENTLY UPDATED' }} + × {% endif %}
{{ name }} + {% set subtopics = topics | selectattr('2', 'equalto', slug) | list %} {% if subtopics %}
- {% for topic in subtopics %} + {% for tslug, tname, _parent in subtopics %} {% endfor %}
@@ -80,11 +80,8 @@

Simulations

Compatibility -
    -
  • -
  • -
  • -
+

Every simulation in this snapshot is HTML5 and runs + on desktop, iPad and Chromebook, so there is nothing to narrow here.

@@ -99,11 +96,9 @@

Simulations

Inclusive Features -
    -
  • -
  • -
  • -
+

Per-simulation accessibility flags are not part of + this snapshot. See the Accessibility + page for the features PhET simulations support.

@@ -158,10 +153,11 @@

Simulations

@@ -173,11 +169,11 @@

Simulations

{% if total_pages > 1 %} {% endif %} @@ -187,6 +183,26 @@

Simulations

+{% elif view_tab == 'customize' %} + {# === Customize mode: simulations that ship a teacher's guide upstream === #} +
+
+
+

Customizable Sims

+ {{ customizable | length }} available in PhET Studio +
+ {% if customizable %} +
+ {% for sim in customizable %} + {% include "_sim_card.html" %} + {% endfor %} +
+ {% else %} +

No customizable simulations in this snapshot.

+ {% endif %} +
+
+ {% else %} {# === Browse mode: grouped by subject (real PhET default) === #}
diff --git a/sites/phet_simulations/templates/teachers.html b/sites/phet_simulations/templates/teachers.html index f1d853d2d..8f925606b 100644 --- a/sites/phet_simulations/templates/teachers.html +++ b/sites/phet_simulations/templates/teachers.html @@ -24,7 +24,7 @@

{{ activ {{ activity.file_type }}

{{ activity.description }}

-

By {{ activity.author }} · {{ activity.download_count }} downloads

+

By {{ activity.author }} · {{ activity.duration_min }} min

{% endfor %} From 73ad5cc56a35654710dfaaea9ba32784e3a5939a Mon Sep 17 00:00:00 2001 From: Zexu Jin Date: Sun, 13 Sep 2026 19:07:47 +0800 Subject: [PATCH 07/10] test(phet_simulations): rebuild the task set and add the grading contract The 43 contributor tasks could not survive the catalogue rebuild: nine of them targeted simulations that do not exist upstream, four more read a play counter that PhET does not publish and that is now gone, one asked which scripts are right-to-left (answerable without opening the site) and one only asked the agent to click through to a language page, leaving nothing to grade. This replaces them with 18 tasks anchored on facts that live on this site and that a model cannot recall: exact version strings, exact release dates, exact per-language and per-facet counts, the related-simulations list, account state. None of those appear on a listing card, so each one requires opening the page that carries it. Adds the reviewer grading contract: verify_lib.py plus verify_0..verify_17, recorded as verifier_path and a rules-only judge_rubric on every row. Ground truth is hardcoded in the verifiers; tasks.jsonl has no answer key. Validation, all deterministic (--no_llm True): - 18 of 18 scripted UI walks pass their verifier; - an 88-cell adversarial matrix matches every expectation - no-op, wrong answer, shortcut without navigation, state mismatch on the two stateful tasks, and an unexpected database write on each read-only task all fail on the intended check; - 16 legitimate rephrasings and alternate routes all still pass. Two verifier defects surfaced during that validation and are fixed here: task 2 rejected a correct answer that echoed the question's own phrase "not offered at university level", and the shared number matcher accepted only digits, so an answer of "Five" was wrongly failed. Co-Authored-By: Claude Opus 5 --- sites/phet_simulations/app.py | 34 +- sites/phet_simulations/tasks.jsonl | 61 +-- .../templates/simulation_detail.html | 4 +- sites/phet_simulations/verify/verify_0.py | 40 ++ sites/phet_simulations/verify/verify_1.py | 35 ++ sites/phet_simulations/verify/verify_10.py | 37 ++ sites/phet_simulations/verify/verify_11.py | 35 ++ sites/phet_simulations/verify/verify_12.py | 36 ++ sites/phet_simulations/verify/verify_13.py | 45 +++ sites/phet_simulations/verify/verify_14.py | 45 +++ sites/phet_simulations/verify/verify_15.py | 36 ++ sites/phet_simulations/verify/verify_16.py | 38 ++ sites/phet_simulations/verify/verify_17.py | 36 ++ sites/phet_simulations/verify/verify_2.py | 46 +++ sites/phet_simulations/verify/verify_3.py | 35 ++ sites/phet_simulations/verify/verify_4.py | 35 ++ sites/phet_simulations/verify/verify_5.py | 37 ++ sites/phet_simulations/verify/verify_6.py | 35 ++ sites/phet_simulations/verify/verify_7.py | 35 ++ sites/phet_simulations/verify/verify_8.py | 36 ++ sites/phet_simulations/verify/verify_9.py | 34 ++ sites/phet_simulations/verify/verify_lib.py | 355 ++++++++++++++++++ 22 files changed, 1073 insertions(+), 57 deletions(-) create mode 100644 sites/phet_simulations/verify/verify_0.py create mode 100644 sites/phet_simulations/verify/verify_1.py create mode 100644 sites/phet_simulations/verify/verify_10.py create mode 100644 sites/phet_simulations/verify/verify_11.py create mode 100644 sites/phet_simulations/verify/verify_12.py create mode 100644 sites/phet_simulations/verify/verify_13.py create mode 100644 sites/phet_simulations/verify/verify_14.py create mode 100644 sites/phet_simulations/verify/verify_15.py create mode 100644 sites/phet_simulations/verify/verify_16.py create mode 100644 sites/phet_simulations/verify/verify_17.py create mode 100644 sites/phet_simulations/verify/verify_2.py create mode 100644 sites/phet_simulations/verify/verify_3.py create mode 100644 sites/phet_simulations/verify/verify_4.py create mode 100644 sites/phet_simulations/verify/verify_5.py create mode 100644 sites/phet_simulations/verify/verify_6.py create mode 100644 sites/phet_simulations/verify/verify_7.py create mode 100644 sites/phet_simulations/verify/verify_8.py create mode 100644 sites/phet_simulations/verify/verify_9.py create mode 100644 sites/phet_simulations/verify/verify_lib.py diff --git a/sites/phet_simulations/app.py b/sites/phet_simulations/app.py index 3cff13f65..955093b14 100644 --- a/sites/phet_simulations/app.py +++ b/sites/phet_simulations/app.py @@ -364,8 +364,7 @@ def simulations_by_subject(slug): @app.route("/simulation/") def simulation_detail(slug): - # Read-only: GET must never mutate the DB, or task answers that - # reference play counts drift between visits. + # Read-only: GET must never mutate the DB. sim = Simulation.query.filter_by(slug=slug).first_or_404() subjects_full = [ @@ -378,17 +377,28 @@ def simulation_detail(slug): l for l in Language.query.filter(Language.code.in_(sim.languages())).all() ] - related = ( - Simulation.query.filter(Simulation.id != sim.id) - .filter( - or_(*[ - Simulation.subjects_json.like(f'%"{s}"%') for s in sim.subjects() - ]) + # Upstream publishes an explicit relatedSimulations list per sim; use it and + # keep its order. Fall back to same-subject titles only when it is empty. + related_slugs = sim.related() + related = [] + if related_slugs: + found = { + s.slug: s for s in + Simulation.query.filter(Simulation.slug.in_(related_slugs)).all() + } + related = [found[s] for s in related_slugs if s in found] + if not related: + related = ( + Simulation.query.filter(Simulation.id != sim.id) + .filter( + or_(*[ + Simulation.subjects_json.like(f'%"{s}"%') for s in sim.subjects() + ]) + ) + .order_by(Simulation.title) + .limit(6) + .all() ) - .order_by(Simulation.title) - .limit(6) - .all() - ) activities = sim.activities.order_by(Activity.published_date.desc()).all() is_saved = ( diff --git a/sites/phet_simulations/tasks.jsonl b/sites/phet_simulations/tasks.jsonl index b55a4dbdb..81d75e30d 100644 --- a/sites/phet_simulations/tasks.jsonl +++ b/sites/phet_simulations/tasks.jsonl @@ -1,43 +1,18 @@ -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--0", "ques": "Find an interactive simulation about Newton's laws and forces that is suitable for elementary school students.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--1", "ques": "Search for a chemistry simulation that helps students learn how to balance chemical equations, and report which grade levels it targets.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--2", "ques": "Browse the Physics category on PhET and list three simulations that cover electromagnetism.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--3", "ques": "Find the 'Gravity and Orbits' simulation and report what subjects it is tagged with, what grade levels it targets, and when it was released.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--4", "ques": "Find a featured biology simulation about evolution and report its short description and target grade levels.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--5", "ques": "Search the PhET catalog for simulations matching 'DNA' and report how many results are returned.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Find the math simulation about probability that is suitable for middle school, high school, and university students, and report its play count.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--7", "ques": "Open the Translations page on PhET, find Chinese (Simplified), and report how many simulations are available in that language.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--8", "ques": "Find all simulations available in Arabic on PhET, and report whether any of them are tagged as 'New'.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--9", "ques": "List three languages PhET simulations are translated into that use a right-to-left script.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--10", "ques": "Filter the simulations catalog to show only Chemistry simulations for high school, sorted by most-played, and report the top result.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--11", "ques": "Find a teacher-submitted activity for the 'Build an Atom' simulation and report its author, grade level, and duration.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--12", "ques": "Browse the For Teachers section, find the most-downloaded activity, and report which simulation it is paired with.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--13", "ques": "Find an earth-science simulation that covers plate tectonics, and list the related simulations recommended on its detail page.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--14", "ques": "On the PhET About page, report the total number of simulations, subject areas, languages, and teacher activities listed.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--15", "ques": "Create a new teacher account on PhET with the email 'test_user@phet.test', then save the 'Energy Skate Park' simulation to your account.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--16", "ques": "Sign in to PhET as teacher@phet.test (password phet-teacher-pass) and report how many simulations are saved to that account.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--17", "ques": "Find a simulation about pH and acids/bases, and identify three subjects or topics it covers.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--18", "ques": "On the PhET catalog, find a simulation about photosynthesis and report which grade levels it targets and how long it takes to run.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--19", "ques": "Browse the Math category on PhET and find a simulation focused on graphing linear equations. Report its release date.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--20", "ques": "Find a chemistry simulation that helps students explore solutions and concentration, and list three related simulations.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--21", "ques": "Filter the catalog to show only simulations tagged 'New', and report how many are released in 2025.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--22", "ques": "Find the 'Circuit Construction Kit: DC' simulation and report the full list of languages it is available in.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--23", "ques": "Browse the biology category and report which simulations are appropriate for university-level students.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--24", "ques": "Look up the elementary-school simulation about magnets that is tagged with both Physics and Earth Science, and report its short description.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--25", "ques": "On the PhET For Teachers page, find a high-school activity longer than 50 minutes and report its title and author.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--26", "ques": "Find a simulation about the greenhouse effect or climate, and report its full overview paragraph.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--27", "ques": "Search for 'genetics' simulations on PhET and report which subjects are covered.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--28", "ques": "Open the 'Wave Interference' simulation page and report its current version number.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--29", "ques": "On the PhET catalog, sort simulations by newest first and list the five most recently released.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--30", "ques": "Filter the catalog to show only elementary-school simulations and report how many are available.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--31", "ques": "Find the biology simulation about predator-prey population cycles that is suitable for middle school students, and report its play count.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--32", "ques": "Browse the PhET catalog and identify a simulation that is tagged as both 'Physics' and 'Math'.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--33", "ques": "Open the Translations page, find Japanese, and click through to view all simulations available in Japanese.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--34", "ques": "Find the 'Natural Selection' simulation and report its target grade levels and three topics it covers.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--35", "ques": "Search for simulations about 'orbit' and report two that come up.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--36", "ques": "Find a teacher activity focused on equivalent fractions and report its target grade level and duration.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--37", "ques": "On the PhET catalog filter activities for high-school level only, and report how many are available.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--38", "ques": "Find a chemistry simulation about isotopes and report what grade levels and languages it supports.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--39", "ques": "Visit the Accessibility page on PhET and report what types of input methods are supported by the simulations.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--40", "ques": "Sign in as student@phet.test (password phet-student-pass), navigate to the 'Build an Atom' simulation, and save it to your account with a note.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--41", "ques": "Find the most-played simulation on the PhET homepage and report its title, subjects, and play count.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--42", "ques": "On the PhET catalog, find a simulation about the water cycle and report its target grade levels and short description.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/"} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--0", "ques": "Filter the PhET catalog to Biology simulations that are suitable for Elementary School, and list the titles you get.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_0.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied both the Biology subject facet and the Elementary School grade facet on the simulations catalog. The final answer MUST list every title the filtered page returns and MUST NOT add titles that the filter excludes. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--1", "ques": "Open the 'Build an Atom' simulation page and report its version number and how many languages it is translated into.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_1.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the named simulation's own detail page; neither figure is shown on a listing page. The final answer MUST state the version string exactly as the page prints it and the number of languages the page reports. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--2", "ques": "Exactly one Biology simulation on PhET is not offered at university level. Find it and report which grade levels it does target.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_2.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have browsed the Biology listing and MUST have opened the detail page of the simulation it names, because grade levels appear only there. The final answer MUST name that one simulation and MUST list exactly the grade bands its page shows, without claiming university level. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--3", "ques": "Use the Release Type filter to show only simulations tagged New, then report how many of them were released during 2025.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_3.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the New release filter. The final answer MUST report how many of those results carry a 2025 release date, which requires checking release dates rather than reporting the size of the whole New set. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--4", "ques": "Sort the PhET catalog by most translated and report the top simulation and its translation count.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_4.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the most-translated sort on the catalog. The final answer MUST name the simulation that sorts first and MUST give its translation count as the site reports it. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--5", "ques": "Find the most recently released simulation in the PhET catalog and report its release date and which subjects it is filed under.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_5.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have used the catalog to establish which simulation is newest and MUST have opened that simulation's detail page. The final answer MUST give its release date and the subjects it is filed under. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Open the Translations page, find Arabic, and report how many simulations are available in that language.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_6.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Translations page. The final answer MUST report the simulation count the page lists for the requested language, not the catalog total and not the count for a regional variant. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--7", "ques": "On the Translations page, find the entry for Arabic (Morocco) and report the number of simulations listed for it.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_7.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Translations page and located the specific regional entry named in the task. The final answer MUST report that entry's simulation count, which differs from the count for the language without a region. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--8", "ques": "Search the PhET catalog for 'quantum'. Report how many results come back, then open the one that was released in 2026 and give its version number.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_8.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have run the site search for the given term and MUST have opened the detail page of the result released in 2026. The final answer MUST report how many results the search returned and the version string shown on that simulation's page. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--9", "ques": "Filter the catalog to the Heat & Thermo topic and report how many simulations it contains.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_9.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the named topic facet on the catalog. The final answer MUST report the number of results that facet returns. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--10", "ques": "Open the 'Plinko Probability' simulation and list the related simulations recommended on its page.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_10.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the named simulation's detail page. The final answer MUST list the simulations shown in that page's related-simulations section, all of them and no others. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--11", "ques": "Open the Customize tab of the PhET simulations catalog and report how many simulations are listed there.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_11.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Customize tab of the simulations catalog. The final answer MUST report how many simulations that tab lists, which is smaller than the full catalog. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--12", "ques": "Sign in to PhET as teacher@phet.test (password phet-teacher-pass) and report how many simulations are saved to that account.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_12.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have signed in with the account named in the task and MUST have opened the account page. The final answer MUST report how many simulations are saved to that account. The run MUST NOT change the saved list. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--13", "ques": "Sign in as student@phet.test (password phet-student-pass), open the 'Membrane Transport' simulation, and save it to your account with a note.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_13.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have signed in as the named student account, opened the named simulation's detail page, and saved it to the account with a note. Exactly one new saved row must exist for that account afterwards, on the named simulation, carrying a non-empty note. No other account's saved list may change."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--14", "ques": "Create a new teacher account on PhET with the email test_user@phet.test, then save the 'Number Pairs' simulation to that account.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_14.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have completed registration with the exact email given in the task and MUST have saved the named simulation to the new account. Exactly one new user row and exactly one new saved row must exist afterwards. An empty answer is a FAIL only if the task also asked for a reported value."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--15", "ques": "Open the teacher Activities list, filter it to Elementary School, and report the title and duration of the activity you find.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_15.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the teacher activities list and applied the Elementary School grade filter. The final answer MUST give the title and the duration in minutes of the activity that filter returns. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--16", "ques": "On the PhET About page, report the total number of simulations, subject areas, languages and teacher activities listed.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_16.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the About page. The final answer MUST report all four published totals: simulations, subject areas, languages and teacher activities. Missing any one of the four is a FAIL. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--17", "ques": "Open 'Build an Atom' and 'Membrane Transport' and report which one is translated into more languages, and the difference between the two counts.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_17.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened both named simulations' detail pages, because the translation count appears only there. The final answer MUST say which of the two is translated into more languages and MUST give the numeric difference between the two counts. An empty answer is a FAIL."} diff --git a/sites/phet_simulations/templates/simulation_detail.html b/sites/phet_simulations/templates/simulation_detail.html index 86fc7913b..805875990 100644 --- a/sites/phet_simulations/templates/simulation_detail.html +++ b/sites/phet_simulations/templates/simulation_detail.html @@ -141,13 +141,13 @@

Available in

-

Statistics

+

Sim details

  • Version: {{ sim.version }}
  • Released: {{ sim.release_date }}
  • Last updated: {{ sim.updated_date }}
  • Translations: {{ sim.locale_count }} languages
  • -
  • Released: {{ sim.release_date }}
  • + {% if sim.is_phet_studio %}
  • Available in PhET Studio
  • {% endif %}
diff --git a/sites/phet_simulations/verify/verify_0.py b/sites/phet_simulations/verify/verify_0.py new file mode 100644 index 000000000..a11d60cd2 --- /dev/null +++ b/sites/phet_simulations/verify/verify_0.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--0. + +Filter to Biology + Elementary School and list the titles. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--0', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_biology_filter", navigated_to(t, "subject=biology"), + f"urls={[u for u in __import__('verify_lib').step_urls(t) if 'simulations' in u][:6]}") + j.check("used_grade_filter", navigated_to(t, "grade=elementary"), "grade=elementary in a visited URL") + j.check("answer_lists_all_three", contains_all(fa, ["Color Vision", "Density", "Natural Selection"]), + f"final={fa!r}") + j.check("answer_excludes_non_matches", + not contains_any(fa, ["Neuron", "Membrane Transport", "Gene Expression", "Molecule Polarity", "pH Scale"]), + f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_1.py b/sites/phet_simulations/verify/verify_1.py new file mode 100644 index 000000000..fb461fc01 --- /dev/null +++ b/sites/phet_simulations/verify/verify_1.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--1. + +Build an Atom: report version and translation count. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--1', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_build_an_atom", navigated_to(t, "/simulation/build-an-atom"), "detail page opened") + j.check("answer_version", contains_all(fa, ["1.9.3"]), f"final={fa!r}") + j.check("answer_translation_count", has_number(fa, 104), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_10.py b/sites/phet_simulations/verify/verify_10.py new file mode 100644 index 000000000..e078b8bc3 --- /dev/null +++ b/sites/phet_simulations/verify/verify_10.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--10. + +Plinko Probability: list the related simulations shown. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--10', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_plinko", navigated_to(t, "/simulation/plinko-probability"), "detail page opened") + j.check("answer_lists_related", contains_all(fa, ["Least-Squares Regression", "Projectile Data Lab", + "Projectile Sampling Distributions", + "Quantum Measurement", "Quantum Coin Toss"]), + f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_11.py b/sites/phet_simulations/verify/verify_11.py new file mode 100644 index 000000000..4f43d7406 --- /dev/null +++ b/sites/phet_simulations/verify/verify_11.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--11. + +Customize tab: how many simulations are listed. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--11', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_customize", navigated_to(t, "view=customize"), "customize tab opened") + j.check("answer_count", has_number(fa, 49), f"numbers={numbers_in(fa)}") + j.check("answer_not_catalog_total", not has_number(fa, 120), "120 is the full catalogue") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_12.py b/sites/phet_simulations/verify/verify_12.py new file mode 100644 index 000000000..b95c65c1d --- /dev/null +++ b/sites/phet_simulations/verify/verify_12.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--12. + +Sign in as the teacher account and report how many sims are saved. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--12', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_login", navigated_to(t, "/login"), "sign-in page visited") + j.check("nav_account", navigated_to(t, "/account"), "account page visited") + j.check("answer_count", has_number(fa, 4), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + saved = saved_sims_for(after, "teacher@phet.test") + j.check("account_still_has_four", saved is not None and len(saved) == 4, f"saved={saved}") + j.check("catalog_unchanged", catalog_unchanged(init, after) is True, "catalogue tables untouched") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_13.py b/sites/phet_simulations/verify/verify_13.py new file mode 100644 index 000000000..5264184c8 --- /dev/null +++ b/sites/phet_simulations/verify/verify_13.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--13. + +Student account saves Membrane Transport with a note. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--13', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_login", navigated_to(t, "/login"), "sign-in page visited") + j.check("nav_membrane_transport", navigated_to(t, "/simulation/membrane-transport"), "detail page opened") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + before = saved_sims_for(init, "student@phet.test") + rows = saved_rows_for(after, "student@phet.test") + j.check("exactly_one_new_save", + before is not None and rows is not None and len(rows) == len(before) + 1, + f"before={before} after={rows}") + j.check("saved_the_named_sim", + rows is not None and any(r[0] == "membrane-transport" for r in rows), f"after={rows}") + j.check("note_is_present", + rows is not None and any(r[0] == "membrane-transport" and r[1].strip() for r in rows), + f"after={rows}") + teacher = saved_sims_for(after, "teacher@phet.test") + j.check("other_account_untouched", teacher is not None and len(teacher) == 4, f"teacher={teacher}") + j.check("catalog_unchanged", catalog_unchanged(init, after) is True, "catalogue tables untouched") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_14.py b/sites/phet_simulations/verify/verify_14.py new file mode 100644 index 000000000..c2a76b3ac --- /dev/null +++ b/sites/phet_simulations/verify/verify_14.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--14. + +Register test_user@phet.test and save Number Pairs to it. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--14', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_register", navigated_to(t, "/register"), "registration page visited") + j.check("nav_number_pairs", navigated_to(t, "/simulation/number-pairs"), "detail page opened") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ci, ca = table_counts(init), table_counts(after) + j.check("exactly_one_new_user", + ci is not None and ca is not None and ca["user"] == ci["user"] + 1, + f"users {ci and ci['user']} -> {ca and ca['user']}") + j.check("new_user_has_the_named_email", user_exists(after, email="test_user@phet.test") is True, + "test_user@phet.test present") + rows = saved_rows_for(after, "test_user@phet.test") + j.check("saved_number_pairs", rows is not None and any(r[0] == "number-pairs" for r in rows), + f"saved={rows}") + j.check("exactly_one_new_save", + ci is not None and ca is not None and ca["saved_simulation"] == ci["saved_simulation"] + 1, + f"saved rows {ci and ci['saved_simulation']} -> {ca and ca['saved_simulation']}") + j.check("catalog_unchanged", catalog_unchanged(init, after) is True, "catalogue tables untouched") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_15.py b/sites/phet_simulations/verify/verify_15.py new file mode 100644 index 000000000..061710a2b --- /dev/null +++ b/sites/phet_simulations/verify/verify_15.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--15. + +Activities filtered to Elementary School: title and duration. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--15', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_activities", navigated_to(t, "/teachers/activities"), "activities list visited") + j.check("used_grade_filter", navigated_to(t, "grade=elementary"), "elementary filter applied") + j.check("answer_title", contains_any(fa, ["Equivalent Fractions Game"]), f"final={fa!r}") + j.check("answer_duration", has_number(fa, 50), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_16.py b/sites/phet_simulations/verify/verify_16.py new file mode 100644 index 000000000..bae506296 --- /dev/null +++ b/sites/phet_simulations/verify/verify_16.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--16. + +About page: the four published totals. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--16', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_about", navigated_to(t, "/about"), "about page visited") + nums = numbers_in(fa) + j.check("answer_simulations", 120 in nums, f"numbers={nums}") + j.check("answer_subjects", 5 in nums, f"numbers={nums}") + j.check("answer_languages", 132 in nums, f"numbers={nums}") + j.check("answer_activities", 14 in nums, f"numbers={nums}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_17.py b/sites/phet_simulations/verify/verify_17.py new file mode 100644 index 000000000..8c88c5461 --- /dev/null +++ b/sites/phet_simulations/verify/verify_17.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--17. + +Compare translation counts of two named simulations. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--17', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_build_an_atom", navigated_to(t, "/simulation/build-an-atom"), "first detail page opened") + j.check("nav_membrane_transport", navigated_to(t, "/simulation/membrane-transport"), "second detail page opened") + j.check("answer_names_winner", contains_any(fa, ["Build an Atom"]), f"final={fa!r}") + j.check("answer_difference", has_number(fa, 73), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_2.py b/sites/phet_simulations/verify/verify_2.py new file mode 100644 index 000000000..6da8f46e3 --- /dev/null +++ b/sites/phet_simulations/verify/verify_2.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--2. + +The only Biology sim not offered at university: report its grade levels. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--2', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_biology_catalog", navigated_any(t, ["subject=biology", "/simulations/category/biology"]), + "biology listing visited") + j.check("nav_natural_selection", navigated_to(t, "/simulation/natural-selection"), "target detail page opened") + j.check("answer_names_target", contains_any(fa, ["Natural Selection"]), f"final={fa!r}") + j.check("answer_grades", contains_all(fa, ["element", "middle", "high"]), f"final={fa!r}") + # The task text itself contains the phrase "not offered at university level", so a + # bare substring test would fail a correct answer that restates the question. Bind to + # the page's own rendering of the band instead, and to claims of targeting it. + low = (fa or "").casefold() + claims_university = ("university (ages 18+)" in low + or "ages 18+" in low + or "targets university" in low + or "including university" in low) + j.check("answer_does_not_claim_university", not claims_university, f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_3.py b/sites/phet_simulations/verify/verify_3.py new file mode 100644 index 000000000..4d9c99f41 --- /dev/null +++ b/sites/phet_simulations/verify/verify_3.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--3. + +New release filter: how many of the New sims were released in 2025. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--3', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_new_filter", navigated_to(t, "release=new"), "release=new in a visited URL") + j.check("answer_count_five", has_number(fa, 5), f"numbers={numbers_in(fa)}") + j.check("answer_not_twelve", not has_number(fa, 12), "12 is the whole New set, not the 2025 subset") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_4.py b/sites/phet_simulations/verify/verify_4.py new file mode 100644 index 000000000..49c46825f --- /dev/null +++ b/sites/phet_simulations/verify/verify_4.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--4. + +Sort by most translated: top simulation and its count. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--4', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_translations_sort", navigated_to(t, "sort=translations"), "sort=translations in a visited URL") + j.check("answer_top_title", contains_any(fa, ["Build an Atom"]), f"final={fa!r}") + j.check("answer_top_count", has_number(fa, 104), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_5.py b/sites/phet_simulations/verify/verify_5.py new file mode 100644 index 000000000..856ca5d1e --- /dev/null +++ b/sites/phet_simulations/verify/verify_5.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--5. + +Most recently released simulation: date and subjects. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--5', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_catalog", navigated_to(t, "/simulations"), "catalogue visited") + j.check("nav_target", navigated_to(t, "/simulation/quantum-wave-interference"), "target detail page opened") + j.check("answer_title", contains_any(fa, ["Quantum Wave Interference"]), f"final={fa!r}") + j.check("answer_release_date", "2026-09-10" in dates_in(fa), f"dates={dates_in(fa)} final={fa!r}") + j.check("answer_subjects", contains_all(fa, ["physics", "chem"]), f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_6.py b/sites/phet_simulations/verify/verify_6.py new file mode 100644 index 000000000..fe123fc18 --- /dev/null +++ b/sites/phet_simulations/verify/verify_6.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--6. + +Translations page: how many simulations are available in Arabic. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--6', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_translations", navigated_to(t, "/translations"), "translations page visited") + j.check("answer_count", has_number(fa, 119), f"numbers={numbers_in(fa)}") + j.check("answer_not_total", not has_number(fa, 120), "120 is the English/total count, not Arabic") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_7.py b/sites/phet_simulations/verify/verify_7.py new file mode 100644 index 000000000..47e529cf7 --- /dev/null +++ b/sites/phet_simulations/verify/verify_7.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--7. + +Translations page: the Arabic (Morocco) entry count. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--7', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_translations", navigated_to(t, "/translations"), "translations page visited") + j.check("answer_count", has_number(fa, 103), f"numbers={numbers_in(fa)}") + j.check("answer_not_plain_arabic", not has_number(fa, 119), "119 is plain Arabic, not Arabic (Morocco)") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_8.py b/sites/phet_simulations/verify/verify_8.py new file mode 100644 index 000000000..d16f55aa5 --- /dev/null +++ b/sites/phet_simulations/verify/verify_8.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--8. + +Search 'quantum': result count, then the 2026 release's version. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--8', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_search", navigated_to(t, "/search"), "search used") + j.check("answer_result_count", has_number(fa, 7), f"numbers={numbers_in(fa)}") + j.check("nav_target", navigated_to(t, "/simulation/quantum-wave-interference"), "2026 release opened") + j.check("answer_version", contains_all(fa, ["1.0.0"]), f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_9.py b/sites/phet_simulations/verify/verify_9.py new file mode 100644 index 000000000..b2ed98ac9 --- /dev/null +++ b/sites/phet_simulations/verify/verify_9.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--9. + +Heat & Thermo topic filter: how many simulations. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--9', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_topic_filter", navigated_to(t, "topic=heat-and-thermo"), "topic facet used") + j.check("answer_count", has_number(fa, 9), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_lib.py b/sites/phet_simulations/verify/verify_lib.py new file mode 100644 index 000000000..d7611dd68 --- /dev/null +++ b/sites/phet_simulations/verify/verify_lib.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +"""verify_lib.py - shared deterministic + LLM utilities for PhET task verification. + +Philosophy: DETERMINISTIC FIRST. + 1. Trajectory navigation check (anti knowledge-shortcut): the agent MUST have + opened the relevant on-site page; a correct answer with no matching navigation + is a memory-recall shortcut = FAIL. + 2. Answer check: exact / regex / token-containment against frozen ground truth. + 3. DB after-state check (stateful tasks): query the SQLite instance DB directly — + the strongest deterministic signal (saved-word list, registered user row). + 4. LLM utilities (text match, screenshot-contains) are used ONLY where exact + matching is brittle, and are ALWAYS anchored on ground truth: the model + verifies *presence* of given content, it never supplies knowledge. One call each. + +Input signature (per task): + --run_dir DIR agent trajectory dir: trajectory.json + screenshots/step_NNN.png + --initial_db PATH initial-state SQLite DB (default: fetched instance_seed from container) + --after_db PATH after-state SQLite DB (default: fetched live instance DB from container) + --container NAME docker container to fetch DBs from (default: $WH_CONTAINER or wh-review) + --no_llm skip LLM-based checks (run deterministic-only) +Output: JSON {task_id, pass, reason, evidence[]} to stdout; exit 0 on PASS, 1 on FAIL. +""" +import base64, json, os, re, sqlite3, subprocess, sys, tempfile, urllib.request +from pathlib import Path +from dataclasses import dataclass + +SITE = "phet_simulations" + +# ---------------------------------------------------------------- trajectory +def load_run(run_dir): + d = Path(run_dir) + traj = json.loads((d / "trajectory.json").read_text()) + traj["_run_dir"] = d + traj["_shots"] = {p.name: p for p in sorted((d / "screenshots").glob("step_*.png"))} + return traj + +def step_urls(traj): + return [s.get("url", "") for s in traj.get("steps", [])] + +def navigated_to(traj, substr, times=1): + """Deterministic: at least `times` trajectory steps have a URL containing substr.""" + return sum(1 for u in step_urls(traj) if substr in u) >= times + +def navigated_any(traj, substrs): + return any(navigated_to(traj, s) for s in substrs) + +def final_answer(traj): + return (traj.get("final_answer") or "").strip() + +def _shot(traj, name): + if not name: + return None + p = traj["_shots"].get(Path(name).name) + return p if (p and p.exists()) else None + +def shot_after_url(traj, substr): + """screenshot_after path of the first step whose URL contains substr.""" + for s in traj.get("steps", []): + if substr in s.get("url", ""): + p = _shot(traj, s.get("screenshot_after")) + if p: + return p + return None + +def last_shot(traj): + for s in reversed(traj.get("steps", [])): + p = _shot(traj, s.get("screenshot_after")) or _shot(traj, s.get("screenshot_before")) + if p: + return p + shots = sorted(traj["_shots"].values()) + return shots[-1] if shots else None + +# ---------------------------------------------------------------- deterministic answer match +def norm(s): + return re.sub(r"\s+", " ", (s or "").strip()).casefold() + +def answer_equals(final, expected): + return norm(final) == norm(expected) + +def contains_all(final, tokens): + f = norm(final) + return all(norm(t) in f for t in tokens) + +def contains_any(final, tokens): + f = norm(final) + return any(norm(t) in f for t in tokens) + +def extract_years(text): + return re.findall(r"\b(1[5-9]\d{2}|20\d{2})\b", text or "") + +def extract_score(text): + m = re.search(r"(\d+)\s*/\s*10", text or "") + return m.group(1) if m else None + +# ---------------------------------------------------------------- DB state +def fetch_db(container, kind): + """kind: 'instance' (after-state) or 'instance_seed' (initial-state). docker cp -> temp file.""" + src = f"{container}:/opt/WebSyn/{SITE}/{kind}/{SITE}.db" + fd, path = tempfile.mkstemp(suffix=".db") + os.close(fd) + r = subprocess.run(["docker", "cp", src, path], capture_output=True, text=True) + if r.returncode != 0: + try: + os.unlink(path) + except OSError: + pass + raise RuntimeError(f"docker cp {src} failed: {r.stderr.strip()}") + return path + +def resolve_db(arg, container, kind): + if arg: + return arg + try: + return fetch_db(container, kind) + except Exception: + return None # caller treats None as "unavailable" and FAILs that check + +def db_query(db_path, sql, params=()): + con = sqlite3.connect(db_path) + try: + return con.execute(sql, params).fetchall() + finally: + con.close() + +def saved_sims_for(db_path, email="teacher@phet.test"): + """Slugs saved to an account, ordered. None when the DB is unavailable.""" + if not db_path: + return None + rows = db_query(db_path, + "SELECT s.slug FROM saved_simulation ss JOIN user u ON u.id=ss.user_id " + "JOIN simulation s ON s.id=ss.sim_id WHERE u.email=? ORDER BY s.slug", (email,)) + return [r[0] for r in rows] + + +def saved_rows_for(db_path, email="teacher@phet.test"): + """(slug, notes) pairs saved to an account.""" + if not db_path: + return None + return db_query(db_path, + "SELECT s.slug, COALESCE(ss.notes, '') FROM saved_simulation ss " + "JOIN user u ON u.id=ss.user_id JOIN simulation s ON s.id=ss.sim_id " + "WHERE u.email=? ORDER BY s.slug", (email,)) + + +def user_exists(db_path, name=None, email=None): + if not db_path: + return None + rows = db_query(db_path, "SELECT name, email FROM user") + return any((name is None or r[0] == name) and (email is None or r[1] == email) + for r in rows) + + +def table_counts(db_path, tables=("simulation", "language", "subject", "grade_level", + "activity", "user", "saved_simulation")): + """Row counts for the runtime tables, used for read-only and exact-delta checks.""" + if not db_path: + return None + out = {} + for tbl in tables: + try: + out[tbl] = db_query(db_path, f"SELECT COUNT(*) FROM {tbl}")[0][0] + except sqlite3.Error: + out[tbl] = None + return out + + +def catalog_unchanged(initial_db, after_db): + """The catalogue tables an agent must never be able to write.""" + if not initial_db or not after_db: + return None + for tbl in ("simulation", "language", "subject", "grade_level", "activity"): + a = db_query(initial_db, f"SELECT COUNT(*) FROM {tbl}")[0][0] + b = db_query(after_db, f"SELECT COUNT(*) FROM {tbl}")[0][0] + if a != b: + return False + return True + + +def read_only_run(initial_db, after_db): + """True when no runtime row changed at all (used by look-up only tasks).""" + if not initial_db or not after_db: + return None + for tbl in ("user", "saved_simulation"): + a = db_query(initial_db, f"SELECT COUNT(*) FROM {tbl}")[0][0] + b = db_query(after_db, f"SELECT COUNT(*) FROM {tbl}")[0][0] + if a != b: + return False + return catalog_unchanged(initial_db, after_db) + + +_WORD_NUMBERS = { + "zero": 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, "six": 6, + "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12, + "thirteen": 13, "fourteen": 14, "fifteen": 15, "sixteen": 16, + "seventeen": 17, "eighteen": 18, "nineteen": 19, "twenty": 20, +} + + +def numbers_in(text): + """Integers in the answer, as digits or as English number words. + + An agent that writes "Five." is as correct as one that writes "5", so both + forms are accepted. Commas inside digit groups are tolerated. + """ + out = [int(m.replace(",", "")) for m in re.findall(r"\b\d[\d,]*\b", text or "")] + for word, value in _WORD_NUMBERS.items(): + if re.search(rf"\b{word}\b", text or "", re.IGNORECASE): + out.append(value) + return out + + +def has_number(text, value): + return value in numbers_in(text) + + +def dates_in(text): + """ISO and common long-form dates, normalised to YYYY-MM-DD where possible.""" + out = list(re.findall(r"\b(\d{4}-\d{2}-\d{2})\b", text or "")) + months = {m: f"{i+1:02d}" for i, m in enumerate( + ["january", "february", "march", "april", "may", "june", "july", + "august", "september", "october", "november", "december"])} + for mon, day, year in re.findall( + r"\b([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})\b", text or ""): + key = mon.casefold() + if key in months: + out.append(f"{year}-{months[key]}-{int(day):02d}") + for day, mon, year in re.findall( + r"\b(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})\b", text or ""): + key = mon.casefold() + if key in months: + out.append(f"{year}-{months[key]}-{int(day):02d}") + return out + + +# ---------------------------------------------------------------- shared LLM utilities (anchored) +# Unified LLM config, same env vars as agent.py / eval_judge.py: +# OPENAI_API_KEY, OPENAI_BASE_URL, JUDGE_MODEL +import simpleArgParser as sap + +# When --no_llm is set (via Judge), the llm_* helpers short-circuit so verifiers +# that call them directly (before j.check(llm=True)) still make ZERO LLM calls. +_NO_LLM = False + + +def _llm_config(): + """Resolve (api_key, api_base, model) from env once per process.""" + key = os.environ.get("OPENAI_API_KEY", "") + base = os.environ.get("OPENAI_BASE_URL", "") + model = os.environ.get("JUDGE_MODEL", "") + return key, base, model + + +def _chat(messages, max_tokens=1024): + """One LLM call against the configured OpenAI-compatible endpoint. Returns text or None.""" + if _NO_LLM: + return None + key, base, model = _llm_config() + if not (key and base and model): + return None # no LLM configured -> callers treat as non-PASS + payload = {"model": model, "messages": messages, + "max_tokens": max_tokens, "temperature": 1.0} + req = urllib.request.Request(base, + data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json", + "Authorization": f"Bearer {key}"}) + try: + data = json.loads(urllib.request.urlopen(req, timeout=180).read()) + except Exception: + return None # caller treats None as a non-PASS; never raises + try: + return data["choices"][0]["message"]["content"] + except Exception: + return None + +def _verdict(out): + """Normalize an LLM reply to (pass_bool, text). None/empty -> (False, '').""" + if not out: + return False, "" + s = out.strip() + return s.upper().startswith("PASS"), s + +def llm_text_match(agent_answer, ground_truth, question): + """One LLM call: does agent_answer correctly answer question AND stay consistent + with the frozen ground truth? The model is given the ground truth as an anchor + and is told NOT to use its own knowledge.""" + if _NO_LLM: + return False, "[skipped: --no_llm]" + out = _chat([{"role": "user", "content": + f"You are a STRICT binary grader.\nQuestion: {question}\n" + f"Ground-truth answer (ANCHOR — judge against THIS, never use your own knowledge): {ground_truth}\n" + f"Agent's answer: {agent_answer}\n" + f"Decide PASS or FAIL ignoring case/punctuation/word order/surrounding prose. " + f"PASS only if the agent's answer is consistent with the ground truth AND actually answers the question. " + f"Line 1: PASS or FAIL. Line 2: one-sentence reason."}]) + return _verdict(out) + +def llm_screenshot_shows(shot_path, must_show, question=""): + """One vision LLM call: does this screenshot visibly render text answering/containing + `must_show`? The model judges pixels only, anchored on the expected content.""" + if _NO_LLM: + return False, "[skipped: --no_llm]" + b64 = base64.b64encode(Path(shot_path).read_bytes()).decode() + out = _chat([{"role": "user", "content": [ + {"type": "text", "text": + f"You are a STRICT binary grader. Only what is VISIBLY rendered in this screenshot counts.\n" + f"Question the page should answer: {question}\n" + f"Expected content to verify PRESENCE of: {must_show}\n" + f"PASS only if the expected content (or a semantically equivalent on-screen answer) is visibly shown. " + f"Do NOT use prior knowledge — judge only the rendered pixels.\n" + f"Line 1: PASS or FAIL. Line 2: quote the visible evidence."}, + {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}]}]) + return _verdict(out) + +# ---------------------------------------------------------------- judge harness + CLI +class Judge: + def __init__(self, task_id, no_llm=False): + global _NO_LLM + _NO_LLM = bool(no_llm) # gate the llm_* helpers at the source + self.task_id = task_id + self.no_llm = no_llm + self.ok = True + self.reason = "" + self.evidence = [] + + def check(self, name, cond, evidence="", llm=False): + if llm and self.no_llm: + self.evidence.append(f"[SKIP] {name} (--no-llm)") + return True + if cond: + self.evidence.append(f"[PASS] {name}: {evidence}") + else: + self.ok = False + if not self.reason: + self.reason = name # record the FIRST failing check + self.evidence.append(f"[FAIL] {name}: {evidence}") + return bool(cond) + + def emit(self): + print(json.dumps({"task_id": self.task_id, "pass": self.ok, + "reason": self.reason, "evidence": self.evidence}, indent=2)) + sys.exit(0 if self.ok else 1) + +def parse_args(): + @dataclass + class VerifyArgs: + run_dir: str = "" + initial_db: str = "" + after_db: str = "" + container: str = os.environ.get("WH_CONTAINER", "wh-review") + no_llm: bool = False + + def post_process(self): + if not self.run_dir: + raise SystemExit("--run_dir is required") + return sap.parse_args(VerifyArgs) From 742b6a0637c15a01ad6e5c9adc6f6c22344f9fb6 Mon Sep 17 00:00:00 2001 From: Zexu Jin Date: Sun, 13 Sep 2026 23:46:22 +0800 Subject: [PATCH 08/10] fix(phet_simulations): make the save note enterable and isolate RTL text Two defects surfaced while reconciling the independent review. The simulation detail page's save form had no note field, yet the API stores a note and a task asks for one. The task was therefore not completable through the UI at all; only a direct API call could satisfy it. Add the field, send it from main.js, and render any existing note back into it. The translations grid applied direction:rtl to the whole card for right-to-left languages, which reversed the English count line as well: Arabic rendered "simulations 119" instead of "119 simulations". Scope the direction to the native-name element and isolate the English name and count. Co-Authored-By: Claude Opus 5 --- sites/phet_simulations/app.py | 13 ++++---- sites/phet_simulations/static/css/style.css | 32 ++++++++++++++++++- sites/phet_simulations/static/js/main.js | 6 +++- .../templates/simulation_detail.html | 4 +++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/sites/phet_simulations/app.py b/sites/phet_simulations/app.py index 955093b14..6a52bdd25 100644 --- a/sites/phet_simulations/app.py +++ b/sites/phet_simulations/app.py @@ -401,13 +401,13 @@ def simulation_detail(slug): ) activities = sim.activities.order_by(Activity.published_date.desc()).all() - is_saved = ( - current_user.is_authenticated - and SavedSimulation.query.filter_by( - user_id=current_user.id, sim_id=sim.id, - ).first() - is not None + saved_row = ( + SavedSimulation.query.filter_by(user_id=current_user.id, sim_id=sim.id).first() + if current_user.is_authenticated + else None ) + is_saved = saved_row is not None + saved_note = saved_row.notes if saved_row else "" return render_template( "simulation_detail.html", @@ -418,6 +418,7 @@ def simulation_detail(slug): related=related, activities=activities, is_saved=is_saved, + saved_note=saved_note, ) diff --git a/sites/phet_simulations/static/css/style.css b/sites/phet_simulations/static/css/style.css index 7eaa24b4a..c66d7f250 100644 --- a/sites/phet_simulations/static/css/style.css +++ b/sites/phet_simulations/static/css/style.css @@ -1003,7 +1003,13 @@ h1, h2, h3, h4 { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } text-decoration: none; } .language-card a:hover { background: var(--c-bg-alt); } -.language-card.rtl a { direction: rtl; text-align: right; } +/* Right-to-left languages: the native name is RTL, but the English language name + and the English "N simulations" line must stay left-to-right. Applying + direction:rtl to the whole card reversed those too, rendering "simulations 103". */ +.language-card.rtl a { text-align: right; } +.language-card.rtl .language-native { direction: rtl; unicode-bidi: isolate; } +.language-card.rtl .language-name, +.language-card.rtl .language-count { direction: ltr; unicode-bidi: isolate; } .language-name { font-weight: 700; font-size: 0.95rem; color: var(--phet-blue); } .language-native { color: var(--c-muted); font-size: 0.85rem; } .language-count { font-size: 0.78rem; color: var(--phet-magenta); margin-top: 0.25rem; font-weight: 700; } @@ -1319,3 +1325,27 @@ img { max-width: 100%; } .detail-tabs { overflow-x: auto; scrollbar-width: thin; -webkit-overflow-scrolling: touch; } .detail-tab { white-space: nowrap; flex: 0 0 auto; } @media (max-width: 520px) { .detail-tabs { gap: 1.25rem; } } + +/* Save-note field on the simulation detail side card (reviewer fix: the task + asks for a note and the API stores one, but the form had no way to enter it). */ +.save-note-label { + display: block; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--c-text-muted); + margin-bottom: 0.3rem; +} +.save-note { + width: 100%; + padding: 0.5rem 0.6rem; + margin-bottom: 0.6rem; + border: 1px solid var(--c-border-strong); + border-radius: var(--radius); + font: inherit; + font-size: 0.88rem; + background: #fff; + color: var(--c-text); +} +.save-note:focus-visible { outline: 2px solid var(--phet-magenta); outline-offset: 1px; } diff --git a/sites/phet_simulations/static/js/main.js b/sites/phet_simulations/static/js/main.js index 7339fe49e..ce4a8d340 100644 --- a/sites/phet_simulations/static/js/main.js +++ b/sites/phet_simulations/static/js/main.js @@ -18,7 +18,11 @@ 'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken(form), }, - body: JSON.stringify({ sim_id: simId }), + body: JSON.stringify( + action === 'save' + ? { sim_id: simId, notes: (form.querySelector('.save-note') || {}).value || '' } + : { sim_id: simId } + ), }) .then(function (r) { return r.json(); }) .then(function (data) { diff --git a/sites/phet_simulations/templates/simulation_detail.html b/sites/phet_simulations/templates/simulation_detail.html index 805875990..809320092 100644 --- a/sites/phet_simulations/templates/simulation_detail.html +++ b/sites/phet_simulations/templates/simulation_detail.html @@ -154,6 +154,10 @@

Sim details

{% if current_user.is_authenticated %}
+ + {% if is_saved %} {% else %} From da142d4996d2a0bf27e83ef8fd829a8770dd2239 Mon Sep 17 00:00:00 2001 From: Zexu Jin Date: Mon, 14 Sep 2026 00:11:51 +0800 Subject: [PATCH 09/10] test(phet_simulations): bind reported counts to what they count The adversarial matrix found three verifiers that a wrong answer could satisfy. verify_13 and verify_14 graded the saved-simulation tasks purely on the database delta, so an answer describing an unrelated simulation still passed once the save itself had happened. Both now require the answer to name the simulation it saved. has_number accepted a digit appearing anywhere in the answer, so "version 9.9.9" satisfied a check for the count 9. Add verify_lib.counts(), which requires the number to be reported as a count of the thing being counted, and use it for the four small-count tasks. It still accepts the natural phrasings: "9 simulations", "nine simulations", "Simulations: 9", "there are 9". Ship the matrix itself under verify/tests/ so the result is reproducible. It is excluded from the image by .dockerignore. 18/18 canonical runs pass, the 90-cell matrix matches every expectation, and six legitimate rephrasings still pass. Co-Authored-By: Claude Opus 5 --- .../verify/tests/adversarial_matrix.py | 112 ++++++++++++++++++ sites/phet_simulations/verify/verify_12.py | 5 +- sites/phet_simulations/verify/verify_13.py | 2 + sites/phet_simulations/verify/verify_14.py | 2 + sites/phet_simulations/verify/verify_3.py | 5 +- sites/phet_simulations/verify/verify_8.py | 5 +- sites/phet_simulations/verify/verify_9.py | 7 +- sites/phet_simulations/verify/verify_lib.py | 23 ++++ 8 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 sites/phet_simulations/verify/tests/adversarial_matrix.py diff --git a/sites/phet_simulations/verify/tests/adversarial_matrix.py b/sites/phet_simulations/verify/tests/adversarial_matrix.py new file mode 100644 index 000000000..e92cd2fb6 --- /dev/null +++ b/sites/phet_simulations/verify/tests/adversarial_matrix.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Adversarial matrix for the PhET grading contract. + +Builds fixtures from the canonical runs and asserts each verifier's verdict. +Fixtures are constructed test inputs, NOT agent runs: they never count as +trajectories and are written to a separate directory. + +Cells per task: + genuine the canonical run -> PASS + noop homepage only, empty answer, unchanged DB -> FAIL + wrong canonical trajectory, answer corrupted -> FAIL + shortcut correct answer, navigation stripped -> FAIL + dirty read-only task, extra row written to after.db -> FAIL + state_missing stateful task, after.db reset to initial -> FAIL +""" +import json, shutil, sqlite3, subprocess, sys, pathlib, re + +CASE = pathlib.Path(__file__).resolve().parent.parent +W = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else + "/Users/jinzexu/Documents/ChatGPT/webharbor/WHR-029-phet-simulations") +RUNS = CASE / "runs/canonical-2026-09-13" +OUT = CASE / "evidence/adversarial-canonical-2026-09-14" +FIX = OUT / "fixtures" +STATEFUL = {13, 14} + +def verdict(n, run_dir, initial, after): + r = subprocess.run( + ["python3", str(W / "sites/phet_simulations/verify" / f"verify_{n}.py"), + "--run_dir", str(run_dir), "--initial_db", str(initial), + "--after_db", str(after), "--no_llm", "True"], + capture_output=True, text=True, cwd=str(W)) + try: + j = json.loads(r.stdout) + return ("PASS" if j["pass"] else "FAIL"), j.get("reason", "") + except Exception: + return "ERROR", (r.stdout + r.stderr)[:160] + +def clone(n, cell): + d = FIX / f"task_{n}_{cell}" + if d.exists(): shutil.rmtree(d) + shutil.copytree(RUNS / f"task_{n}", d) + return d + +def main(): + FIX.mkdir(parents=True, exist_ok=True) + rows = [] + for n in range(18): + src = RUNS / f"task_{n}" + base_i, base_a = src / "initial.db", src / "after.db" + + v, why = verdict(n, src, base_i, base_a) + rows.append({"task": n, "cell": "genuine", "expected": "PASS", "verdict": v, + "match": v == "PASS", "reason": why}) + + d = clone(n, "noop") + t = json.loads((d / "trajectory.json").read_text()) + t["steps"] = [s for s in t["steps"] if False] + t["final_answer"] = "" + t["success_self_report"] = False + (d / "trajectory.json").write_text(json.dumps(t, indent=2)) + shutil.copy(base_i, d / "after.db") + v, why = verdict(n, d, base_i, d / "after.db") + rows.append({"task": n, "cell": "noop", "expected": "FAIL", "verdict": v, + "match": v == "FAIL", "reason": why}) + + # Replace the answer wholesale rather than perturbing digits. Shifting the + # numbers inside an answer can leave every fact the task actually asks for + # intact (task 2 only had incidental age ranges to corrupt), which tests + # nothing. + d = clone(n, "wrong") + t = json.loads((d / "trajectory.json").read_text()) + t["final_answer"] = "Quantum Coin Toss, version 9.9.9, translated into 3 languages." + (d / "trajectory.json").write_text(json.dumps(t, indent=2)) + v, why = verdict(n, d, base_i, base_a) + rows.append({"task": n, "cell": "wrong", "expected": "FAIL", "verdict": v, + "match": v == "FAIL", "reason": why}) + + d = clone(n, "shortcut") + t = json.loads((d / "trajectory.json").read_text()) + t["steps"] = [] + (d / "trajectory.json").write_text(json.dumps(t, indent=2)) + v, why = verdict(n, d, base_i, base_a) + rows.append({"task": n, "cell": "shortcut", "expected": "FAIL", "verdict": v, + "match": v == "FAIL", "reason": why}) + + if n not in STATEFUL: + d = clone(n, "dirty") + shutil.copy(base_a, d / "after.db") + con = sqlite3.connect(d / "after.db") + con.execute("INSERT INTO saved_simulation (user_id, sim_id, notes, saved_at) " + "VALUES (1, 5, 'adversarial fixture', '2026-01-01 00:00:00')") + con.commit(); con.close() + v, why = verdict(n, d, base_i, d / "after.db") + rows.append({"task": n, "cell": "dirty", "expected": "FAIL", "verdict": v, + "match": v == "FAIL", "reason": why}) + else: + d = clone(n, "state_missing") + shutil.copy(base_i, d / "after.db") + v, why = verdict(n, d, base_i, d / "after.db") + rows.append({"task": n, "cell": "state_missing", "expected": "FAIL", "verdict": v, + "match": v == "FAIL", "reason": why}) + + OUT.mkdir(parents=True, exist_ok=True) + (OUT / "results.json").write_text(json.dumps(rows, indent=1)) + ok = sum(1 for r in rows if r["match"]) + print(f"{ok}/{len(rows)} cells match expectation") + for r in rows: + if not r["match"]: + print(f" MISMATCH task {r['task']} {r['cell']}: expected {r['expected']}, got {r['verdict']} ({r['reason']})") + return 0 if ok == len(rows) else 1 + +sys.exit(main()) diff --git a/sites/phet_simulations/verify/verify_12.py b/sites/phet_simulations/verify/verify_12.py index b95c65c1d..8384db163 100644 --- a/sites/phet_simulations/verify/verify_12.py +++ b/sites/phet_simulations/verify/verify_12.py @@ -12,7 +12,7 @@ contains_all, contains_any, answer_equals, numbers_in, has_number, dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, read_only_run, catalog_unchanged, table_counts, db_query, - llm_text_match, Judge, parse_args) + llm_text_match, counts, Judge, parse_args) def main(): @@ -23,7 +23,8 @@ def main(): j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") j.check("nav_login", navigated_to(t, "/login"), "sign-in page visited") j.check("nav_account", navigated_to(t, "/account"), "account page visited") - j.check("answer_count", has_number(fa, 4), f"numbers={numbers_in(fa)}") + j.check("answer_count", counts(fa, 4, "simulation", "sim", "saved", "item"), + f"the count must be reported as a number of saved simulations; numbers={numbers_in(fa)} final={fa!r}") init = resolve_db(a.initial_db, a.container, "instance_seed") after = resolve_db(a.after_db, a.container, "instance") saved = saved_sims_for(after, "teacher@phet.test") diff --git a/sites/phet_simulations/verify/verify_13.py b/sites/phet_simulations/verify/verify_13.py index 5264184c8..7522656bc 100644 --- a/sites/phet_simulations/verify/verify_13.py +++ b/sites/phet_simulations/verify/verify_13.py @@ -21,6 +21,8 @@ def main(): t = load_run(a.run_dir) fa = final_answer(t) j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("answer_names_saved_sim", contains_any(fa, ["Membrane Transport"]), + f"the final answer must say which simulation was saved; final={fa!r}") j.check("nav_login", navigated_to(t, "/login"), "sign-in page visited") j.check("nav_membrane_transport", navigated_to(t, "/simulation/membrane-transport"), "detail page opened") init = resolve_db(a.initial_db, a.container, "instance_seed") diff --git a/sites/phet_simulations/verify/verify_14.py b/sites/phet_simulations/verify/verify_14.py index c2a76b3ac..09402e765 100644 --- a/sites/phet_simulations/verify/verify_14.py +++ b/sites/phet_simulations/verify/verify_14.py @@ -21,6 +21,8 @@ def main(): t = load_run(a.run_dir) fa = final_answer(t) j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("answer_names_saved_sim", contains_any(fa, ["Number Pairs"]), + f"the final answer must say which simulation was saved; final={fa!r}") j.check("nav_register", navigated_to(t, "/register"), "registration page visited") j.check("nav_number_pairs", navigated_to(t, "/simulation/number-pairs"), "detail page opened") init = resolve_db(a.initial_db, a.container, "instance_seed") diff --git a/sites/phet_simulations/verify/verify_3.py b/sites/phet_simulations/verify/verify_3.py index 4d9c99f41..1d9fdd661 100644 --- a/sites/phet_simulations/verify/verify_3.py +++ b/sites/phet_simulations/verify/verify_3.py @@ -12,7 +12,7 @@ contains_all, contains_any, answer_equals, numbers_in, has_number, dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, read_only_run, catalog_unchanged, table_counts, db_query, - llm_text_match, Judge, parse_args) + llm_text_match, counts, Judge, parse_args) def main(): @@ -22,7 +22,8 @@ def main(): fa = final_answer(t) j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") j.check("used_new_filter", navigated_to(t, "release=new"), "release=new in a visited URL") - j.check("answer_count_five", has_number(fa, 5), f"numbers={numbers_in(fa)}") + j.check("answer_count_five", counts(fa, 5, "simulation", "sim", "release", "new"), + f"the count must be reported as a number of simulations; numbers={numbers_in(fa)} final={fa!r}") j.check("answer_not_twelve", not has_number(fa, 12), "12 is the whole New set, not the 2025 subset") init = resolve_db(a.initial_db, a.container, "instance_seed") after = resolve_db(a.after_db, a.container, "instance") diff --git a/sites/phet_simulations/verify/verify_8.py b/sites/phet_simulations/verify/verify_8.py index d16f55aa5..a91bb6bb3 100644 --- a/sites/phet_simulations/verify/verify_8.py +++ b/sites/phet_simulations/verify/verify_8.py @@ -12,7 +12,7 @@ contains_all, contains_any, answer_equals, numbers_in, has_number, dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, read_only_run, catalog_unchanged, table_counts, db_query, - llm_text_match, Judge, parse_args) + llm_text_match, counts, Judge, parse_args) def main(): @@ -22,7 +22,8 @@ def main(): fa = final_answer(t) j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") j.check("used_search", navigated_to(t, "/search"), "search used") - j.check("answer_result_count", has_number(fa, 7), f"numbers={numbers_in(fa)}") + j.check("answer_result_count", counts(fa, 7, "result", "simulation", "sim", "hit", "match"), + f"the count must be reported as a number of results; numbers={numbers_in(fa)} final={fa!r}") j.check("nav_target", navigated_to(t, "/simulation/quantum-wave-interference"), "2026 release opened") j.check("answer_version", contains_all(fa, ["1.0.0"]), f"final={fa!r}") init = resolve_db(a.initial_db, a.container, "instance_seed") diff --git a/sites/phet_simulations/verify/verify_9.py b/sites/phet_simulations/verify/verify_9.py index b2ed98ac9..40597b9bc 100644 --- a/sites/phet_simulations/verify/verify_9.py +++ b/sites/phet_simulations/verify/verify_9.py @@ -12,7 +12,7 @@ contains_all, contains_any, answer_equals, numbers_in, has_number, dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, read_only_run, catalog_unchanged, table_counts, db_query, - llm_text_match, Judge, parse_args) + llm_text_match, counts, Judge, parse_args) def main(): @@ -22,7 +22,10 @@ def main(): fa = final_answer(t) j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") j.check("used_topic_filter", navigated_to(t, "topic=heat-and-thermo"), "topic facet used") - j.check("answer_count", has_number(fa, 9), f"numbers={numbers_in(fa)}") + j.check("answer_count", counts(fa, 9, "simulation", "sim", "result"), + f"the count must be reported as a number of simulations; numbers={numbers_in(fa)} final={fa!r}") + j.check("answer_names_topic", contains_any(fa, ["Heat", "Thermo"]), + f"the answer must say which topic was counted; final={fa!r}") init = resolve_db(a.initial_db, a.container, "instance_seed") after = resolve_db(a.after_db, a.container, "instance") ro = read_only_run(init, after) diff --git a/sites/phet_simulations/verify/verify_lib.py b/sites/phet_simulations/verify/verify_lib.py index d7611dd68..263b77247 100644 --- a/sites/phet_simulations/verify/verify_lib.py +++ b/sites/phet_simulations/verify/verify_lib.py @@ -213,6 +213,29 @@ def has_number(text, value): return value in numbers_in(text) +def counts(text, value, *nouns): + """True when `value` is reported AS A COUNT of one of `nouns`. + + has_number alone accepts a digit appearing anywhere, so an answer about a + different subject ("version 9.9.9") satisfies a check for 9. This binds the + number to its referent while still accepting the natural phrasings an agent + uses: "9 simulations", "9 sims", "nine simulations", "simulations: 9", + "contains 9", "there are 9". + """ + t = (text or "").lower() + if value not in numbers_in(text): + return False + words = {v: k for k, v in _WORD_NUMBERS.items()} + forms = [str(value)] + ([words[value]] if value in words else []) + for noun in [n.lower() for n in nouns]: + for f in forms: + pats = [rf"{re.escape(f)}\s*(?:\w+\s+){{0,3}}{re.escape(noun)}", + rf"{re.escape(noun)}[^.]{{0,40}}?\b{re.escape(f)}\b"] + if any(re.search(p, t) for p in pats): + return True + return False + + def dates_in(text): """ISO and common long-form dates, normalised to YYYY-MM-DD where possible.""" out = list(re.findall(r"\b(\d{4}-\d{2}-\d{2})\b", text or "")) From 751fcd70e0d75af2228bf49c0b09bdfa1107b079 Mon Sep 17 00:00:00 2001 From: Zexu Jin Date: Mon, 14 Sep 2026 00:18:17 +0800 Subject: [PATCH 10/10] chore(phet_simulations): register as site 30 on port 40029 and pin assets Upstream merged fedex, webmd_doctor, healthline, kaggle and nvidia while this branch was in review, so take the next free slot. .assets-revision points at ChilleD/WebHarbor discussions/86 so this branch fetches and builds without waiting for that PR to merge. That commit is upstream's current pin b7e605c plus phet_simulations.tar.gz, with every other archive byte-identical - checked by comparing the file OIDs of both trees, which matters because the earlier asset PR #79 predated the NVIDIA repin and would have regressed those images. Temporary; repin to the merge commit once #86 lands. Co-Authored-By: Claude Opus 5 --- .assets-revision | 7 ++- sites/phet_simulations/_health 2.py | 11 +++++ sites/phet_simulations/app.py | 2 +- sites/phet_simulations/tasks.jsonl | 36 +++++++-------- sites/phet_simulations/verify/verify_0 2.py | 40 +++++++++++++++++ sites/phet_simulations/verify/verify_1 2.py | 35 +++++++++++++++ sites/phet_simulations/verify/verify_10 2.py | 37 +++++++++++++++ sites/phet_simulations/verify/verify_11 2.py | 35 +++++++++++++++ sites/phet_simulations/verify/verify_12 2.py | 37 +++++++++++++++ sites/phet_simulations/verify/verify_13 2.py | 47 ++++++++++++++++++++ sites/phet_simulations/verify/verify_14 2.py | 47 ++++++++++++++++++++ sites/phet_simulations/verify/verify_15 2.py | 36 +++++++++++++++ sites/phet_simulations/verify/verify_16 2.py | 38 ++++++++++++++++ sites/phet_simulations/verify/verify_17 2.py | 36 +++++++++++++++ sites/phet_simulations/verify/verify_2 2.py | 46 +++++++++++++++++++ sites/phet_simulations/verify/verify_3 2.py | 36 +++++++++++++++ sites/phet_simulations/verify/verify_4 2.py | 35 +++++++++++++++ sites/phet_simulations/verify/verify_5 2.py | 37 +++++++++++++++ sites/phet_simulations/verify/verify_6 2.py | 35 +++++++++++++++ sites/phet_simulations/verify/verify_7 2.py | 35 +++++++++++++++ sites/phet_simulations/verify/verify_8 2.py | 37 +++++++++++++++ sites/phet_simulations/verify/verify_9 2.py | 37 +++++++++++++++ 22 files changed, 722 insertions(+), 20 deletions(-) create mode 100644 sites/phet_simulations/_health 2.py create mode 100644 sites/phet_simulations/verify/verify_0 2.py create mode 100644 sites/phet_simulations/verify/verify_1 2.py create mode 100644 sites/phet_simulations/verify/verify_10 2.py create mode 100644 sites/phet_simulations/verify/verify_11 2.py create mode 100644 sites/phet_simulations/verify/verify_12 2.py create mode 100644 sites/phet_simulations/verify/verify_13 2.py create mode 100644 sites/phet_simulations/verify/verify_14 2.py create mode 100644 sites/phet_simulations/verify/verify_15 2.py create mode 100644 sites/phet_simulations/verify/verify_16 2.py create mode 100644 sites/phet_simulations/verify/verify_17 2.py create mode 100644 sites/phet_simulations/verify/verify_2 2.py create mode 100644 sites/phet_simulations/verify/verify_3 2.py create mode 100644 sites/phet_simulations/verify/verify_4 2.py create mode 100644 sites/phet_simulations/verify/verify_5 2.py create mode 100644 sites/phet_simulations/verify/verify_6 2.py create mode 100644 sites/phet_simulations/verify/verify_7 2.py create mode 100644 sites/phet_simulations/verify/verify_8 2.py create mode 100644 sites/phet_simulations/verify/verify_9 2.py diff --git a/.assets-revision b/.assets-revision index ab6fabd3a..6f6b0fe27 100644 --- a/.assets-revision +++ b/.assets-revision @@ -43,4 +43,9 @@ # superseded 11278264-byte pack, sha256 c533c283...) no longer applies and # `scripts/fetch_assets.sh` installs sites/kaggle's assets from this pin. repo: ChilleD/WebHarbor -revision: b7e605c0ec5fc47de85b09e7427162cc50e38980 +# TEMPORARY pin to the reviewer asset PR so this branch builds standalone: +# ChilleD/WebHarbor discussions/86. That commit is upstream's current pin +# (b7e605c) plus phet_simulations.tar.gz; every other archive is byte- +# identical, verified by comparing file OIDs of both trees. +# Repin to the merge commit once #86 lands. +revision: 9c6ab2169d64f52cdee44a12921efc1e9061a1ee diff --git a/sites/phet_simulations/_health 2.py b/sites/phet_simulations/_health 2.py new file mode 100644 index 000000000..df949a48a --- /dev/null +++ b/sites/phet_simulations/_health 2.py @@ -0,0 +1,11 @@ +"""Simple health probe for the PhET Simulations mirror. + +Returned by the /_health endpoint. The control plane only inspects HTTP +status, so any 2xx response with a JSON body is sufficient — the payload +shape mirrors the scaffold default and is also surfaced verbatim to +human reviewers. +""" + + +def health(): + return {"ok": True, "site": "phet_simulations"} diff --git a/sites/phet_simulations/app.py b/sites/phet_simulations/app.py index 6a52bdd25..0d4eb741f 100644 --- a/sites/phet_simulations/app.py +++ b/sites/phet_simulations/app.py @@ -846,5 +846,5 @@ def seed_all(): if __name__ == "__main__": - port = int(os.environ.get("PORT", 40015)) + port = int(os.environ.get("PORT", 40029)) app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/phet_simulations/tasks.jsonl b/sites/phet_simulations/tasks.jsonl index 81d75e30d..c44e8e744 100644 --- a/sites/phet_simulations/tasks.jsonl +++ b/sites/phet_simulations/tasks.jsonl @@ -1,18 +1,18 @@ -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--0", "ques": "Filter the PhET catalog to Biology simulations that are suitable for Elementary School, and list the titles you get.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_0.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied both the Biology subject facet and the Elementary School grade facet on the simulations catalog. The final answer MUST list every title the filtered page returns and MUST NOT add titles that the filter excludes. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--1", "ques": "Open the 'Build an Atom' simulation page and report its version number and how many languages it is translated into.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_1.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the named simulation's own detail page; neither figure is shown on a listing page. The final answer MUST state the version string exactly as the page prints it and the number of languages the page reports. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--2", "ques": "Exactly one Biology simulation on PhET is not offered at university level. Find it and report which grade levels it does target.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_2.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have browsed the Biology listing and MUST have opened the detail page of the simulation it names, because grade levels appear only there. The final answer MUST name that one simulation and MUST list exactly the grade bands its page shows, without claiming university level. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--3", "ques": "Use the Release Type filter to show only simulations tagged New, then report how many of them were released during 2025.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_3.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the New release filter. The final answer MUST report how many of those results carry a 2025 release date, which requires checking release dates rather than reporting the size of the whole New set. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--4", "ques": "Sort the PhET catalog by most translated and report the top simulation and its translation count.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_4.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the most-translated sort on the catalog. The final answer MUST name the simulation that sorts first and MUST give its translation count as the site reports it. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--5", "ques": "Find the most recently released simulation in the PhET catalog and report its release date and which subjects it is filed under.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_5.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have used the catalog to establish which simulation is newest and MUST have opened that simulation's detail page. The final answer MUST give its release date and the subjects it is filed under. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Open the Translations page, find Arabic, and report how many simulations are available in that language.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_6.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Translations page. The final answer MUST report the simulation count the page lists for the requested language, not the catalog total and not the count for a regional variant. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--7", "ques": "On the Translations page, find the entry for Arabic (Morocco) and report the number of simulations listed for it.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_7.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Translations page and located the specific regional entry named in the task. The final answer MUST report that entry's simulation count, which differs from the count for the language without a region. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--8", "ques": "Search the PhET catalog for 'quantum'. Report how many results come back, then open the one that was released in 2026 and give its version number.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_8.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have run the site search for the given term and MUST have opened the detail page of the result released in 2026. The final answer MUST report how many results the search returned and the version string shown on that simulation's page. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--9", "ques": "Filter the catalog to the Heat & Thermo topic and report how many simulations it contains.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_9.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the named topic facet on the catalog. The final answer MUST report the number of results that facet returns. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--10", "ques": "Open the 'Plinko Probability' simulation and list the related simulations recommended on its page.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_10.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the named simulation's detail page. The final answer MUST list the simulations shown in that page's related-simulations section, all of them and no others. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--11", "ques": "Open the Customize tab of the PhET simulations catalog and report how many simulations are listed there.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_11.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Customize tab of the simulations catalog. The final answer MUST report how many simulations that tab lists, which is smaller than the full catalog. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--12", "ques": "Sign in to PhET as teacher@phet.test (password phet-teacher-pass) and report how many simulations are saved to that account.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_12.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have signed in with the account named in the task and MUST have opened the account page. The final answer MUST report how many simulations are saved to that account. The run MUST NOT change the saved list. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--13", "ques": "Sign in as student@phet.test (password phet-student-pass), open the 'Membrane Transport' simulation, and save it to your account with a note.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_13.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have signed in as the named student account, opened the named simulation's detail page, and saved it to the account with a note. Exactly one new saved row must exist for that account afterwards, on the named simulation, carrying a non-empty note. No other account's saved list may change."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--14", "ques": "Create a new teacher account on PhET with the email test_user@phet.test, then save the 'Number Pairs' simulation to that account.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_14.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have completed registration with the exact email given in the task and MUST have saved the named simulation to the new account. Exactly one new user row and exactly one new saved row must exist afterwards. An empty answer is a FAIL only if the task also asked for a reported value."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--15", "ques": "Open the teacher Activities list, filter it to Elementary School, and report the title and duration of the activity you find.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_15.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the teacher activities list and applied the Elementary School grade filter. The final answer MUST give the title and the duration in minutes of the activity that filter returns. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--16", "ques": "On the PhET About page, report the total number of simulations, subject areas, languages and teacher activities listed.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_16.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the About page. The final answer MUST report all four published totals: simulations, subject areas, languages and teacher activities. Missing any one of the four is a FAIL. An empty answer is a FAIL."} -{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--17", "ques": "Open 'Build an Atom' and 'Membrane Transport' and report which one is translated into more languages, and the difference between the two counts.", "web": "http://localhost:40028/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_17.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened both named simulations' detail pages, because the translation count appears only there. The final answer MUST say which of the two is translated into more languages and MUST give the numeric difference between the two counts. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--0", "ques": "Filter the PhET catalog to Biology simulations that are suitable for Elementary School, and list the titles you get.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_0.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied both the Biology subject facet and the Elementary School grade facet on the simulations catalog. The final answer MUST list every title the filtered page returns and MUST NOT add titles that the filter excludes. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--1", "ques": "Open the 'Build an Atom' simulation page and report its version number and how many languages it is translated into.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_1.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the named simulation's own detail page; neither figure is shown on a listing page. The final answer MUST state the version string exactly as the page prints it and the number of languages the page reports. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--2", "ques": "Exactly one Biology simulation on PhET is not offered at university level. Find it and report which grade levels it does target.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_2.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have browsed the Biology listing and MUST have opened the detail page of the simulation it names, because grade levels appear only there. The final answer MUST name that one simulation and MUST list exactly the grade bands its page shows, without claiming university level. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--3", "ques": "Use the Release Type filter to show only simulations tagged New, then report how many of them were released during 2025.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_3.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the New release filter. The final answer MUST report how many of those results carry a 2025 release date, which requires checking release dates rather than reporting the size of the whole New set. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--4", "ques": "Sort the PhET catalog by most translated and report the top simulation and its translation count.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_4.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the most-translated sort on the catalog. The final answer MUST name the simulation that sorts first and MUST give its translation count as the site reports it. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--5", "ques": "Find the most recently released simulation in the PhET catalog and report its release date and which subjects it is filed under.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_5.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have used the catalog to establish which simulation is newest and MUST have opened that simulation's detail page. The final answer MUST give its release date and the subjects it is filed under. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--6", "ques": "Open the Translations page, find Arabic, and report how many simulations are available in that language.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_6.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Translations page. The final answer MUST report the simulation count the page lists for the requested language, not the catalog total and not the count for a regional variant. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--7", "ques": "On the Translations page, find the entry for Arabic (Morocco) and report the number of simulations listed for it.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_7.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Translations page and located the specific regional entry named in the task. The final answer MUST report that entry's simulation count, which differs from the count for the language without a region. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--8", "ques": "Search the PhET catalog for 'quantum'. Report how many results come back, then open the one that was released in 2026 and give its version number.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_8.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have run the site search for the given term and MUST have opened the detail page of the result released in 2026. The final answer MUST report how many results the search returned and the version string shown on that simulation's page. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--9", "ques": "Filter the catalog to the Heat & Thermo topic and report how many simulations it contains.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_9.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have applied the named topic facet on the catalog. The final answer MUST report the number of results that facet returns. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--10", "ques": "Open the 'Plinko Probability' simulation and list the related simulations recommended on its page.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_10.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the named simulation's detail page. The final answer MUST list the simulations shown in that page's related-simulations section, all of them and no others. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--11", "ques": "Open the Customize tab of the PhET simulations catalog and report how many simulations are listed there.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_11.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the Customize tab of the simulations catalog. The final answer MUST report how many simulations that tab lists, which is smaller than the full catalog. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--12", "ques": "Sign in to PhET as teacher@phet.test (password phet-teacher-pass) and report how many simulations are saved to that account.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_12.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have signed in with the account named in the task and MUST have opened the account page. The final answer MUST report how many simulations are saved to that account. The run MUST NOT change the saved list. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--13", "ques": "Sign in as student@phet.test (password phet-student-pass), open the 'Membrane Transport' simulation, and save it to your account with a note.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_13.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have signed in as the named student account, opened the named simulation's detail page, and saved it to the account with a note. Exactly one new saved row must exist for that account afterwards, on the named simulation, carrying a non-empty note. No other account's saved list may change."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--14", "ques": "Create a new teacher account on PhET with the email test_user@phet.test, then save the 'Number Pairs' simulation to that account.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_14.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have completed registration with the exact email given in the task and MUST have saved the named simulation to the new account. Exactly one new user row and exactly one new saved row must exist afterwards. An empty answer is a FAIL only if the task also asked for a reported value."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--15", "ques": "Open the teacher Activities list, filter it to Elementary School, and report the title and duration of the activity you find.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_15.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the teacher activities list and applied the Elementary School grade filter. The final answer MUST give the title and the duration in minutes of the activity that filter returns. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--16", "ques": "On the PhET About page, report the total number of simulations, subject areas, languages and teacher activities listed.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_16.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened the About page. The final answer MUST report all four published totals: simulations, subject areas, languages and teacher activities. Missing any one of the four is a FAIL. An empty answer is a FAIL."} +{"web_name": "PhET Interactive Simulations", "id": "PhET Interactive Simulations--17", "ques": "Open 'Build an Atom' and 'Membrane Transport' and report which one is translated into more languages, and the difference between the two counts.", "web": "http://localhost:40029/", "upstream_url": "https://phet.colorado.edu/", "verifier_path": "sites/phet_simulations/verify/verify_17.py", "judge_rubric": "FACT CHECKPOINTS. The agent MUST have opened both named simulations' detail pages, because the translation count appears only there. The final answer MUST say which of the two is translated into more languages and MUST give the numeric difference between the two counts. An empty answer is a FAIL."} diff --git a/sites/phet_simulations/verify/verify_0 2.py b/sites/phet_simulations/verify/verify_0 2.py new file mode 100644 index 000000000..a11d60cd2 --- /dev/null +++ b/sites/phet_simulations/verify/verify_0 2.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--0. + +Filter to Biology + Elementary School and list the titles. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--0', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_biology_filter", navigated_to(t, "subject=biology"), + f"urls={[u for u in __import__('verify_lib').step_urls(t) if 'simulations' in u][:6]}") + j.check("used_grade_filter", navigated_to(t, "grade=elementary"), "grade=elementary in a visited URL") + j.check("answer_lists_all_three", contains_all(fa, ["Color Vision", "Density", "Natural Selection"]), + f"final={fa!r}") + j.check("answer_excludes_non_matches", + not contains_any(fa, ["Neuron", "Membrane Transport", "Gene Expression", "Molecule Polarity", "pH Scale"]), + f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_1 2.py b/sites/phet_simulations/verify/verify_1 2.py new file mode 100644 index 000000000..fb461fc01 --- /dev/null +++ b/sites/phet_simulations/verify/verify_1 2.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--1. + +Build an Atom: report version and translation count. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--1', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_build_an_atom", navigated_to(t, "/simulation/build-an-atom"), "detail page opened") + j.check("answer_version", contains_all(fa, ["1.9.3"]), f"final={fa!r}") + j.check("answer_translation_count", has_number(fa, 104), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_10 2.py b/sites/phet_simulations/verify/verify_10 2.py new file mode 100644 index 000000000..e078b8bc3 --- /dev/null +++ b/sites/phet_simulations/verify/verify_10 2.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--10. + +Plinko Probability: list the related simulations shown. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--10', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_plinko", navigated_to(t, "/simulation/plinko-probability"), "detail page opened") + j.check("answer_lists_related", contains_all(fa, ["Least-Squares Regression", "Projectile Data Lab", + "Projectile Sampling Distributions", + "Quantum Measurement", "Quantum Coin Toss"]), + f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_11 2.py b/sites/phet_simulations/verify/verify_11 2.py new file mode 100644 index 000000000..4f43d7406 --- /dev/null +++ b/sites/phet_simulations/verify/verify_11 2.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--11. + +Customize tab: how many simulations are listed. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--11', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_customize", navigated_to(t, "view=customize"), "customize tab opened") + j.check("answer_count", has_number(fa, 49), f"numbers={numbers_in(fa)}") + j.check("answer_not_catalog_total", not has_number(fa, 120), "120 is the full catalogue") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_12 2.py b/sites/phet_simulations/verify/verify_12 2.py new file mode 100644 index 000000000..8384db163 --- /dev/null +++ b/sites/phet_simulations/verify/verify_12 2.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--12. + +Sign in as the teacher account and report how many sims are saved. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, counts, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--12', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_login", navigated_to(t, "/login"), "sign-in page visited") + j.check("nav_account", navigated_to(t, "/account"), "account page visited") + j.check("answer_count", counts(fa, 4, "simulation", "sim", "saved", "item"), + f"the count must be reported as a number of saved simulations; numbers={numbers_in(fa)} final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + saved = saved_sims_for(after, "teacher@phet.test") + j.check("account_still_has_four", saved is not None and len(saved) == 4, f"saved={saved}") + j.check("catalog_unchanged", catalog_unchanged(init, after) is True, "catalogue tables untouched") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_13 2.py b/sites/phet_simulations/verify/verify_13 2.py new file mode 100644 index 000000000..7522656bc --- /dev/null +++ b/sites/phet_simulations/verify/verify_13 2.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--13. + +Student account saves Membrane Transport with a note. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--13', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("answer_names_saved_sim", contains_any(fa, ["Membrane Transport"]), + f"the final answer must say which simulation was saved; final={fa!r}") + j.check("nav_login", navigated_to(t, "/login"), "sign-in page visited") + j.check("nav_membrane_transport", navigated_to(t, "/simulation/membrane-transport"), "detail page opened") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + before = saved_sims_for(init, "student@phet.test") + rows = saved_rows_for(after, "student@phet.test") + j.check("exactly_one_new_save", + before is not None and rows is not None and len(rows) == len(before) + 1, + f"before={before} after={rows}") + j.check("saved_the_named_sim", + rows is not None and any(r[0] == "membrane-transport" for r in rows), f"after={rows}") + j.check("note_is_present", + rows is not None and any(r[0] == "membrane-transport" and r[1].strip() for r in rows), + f"after={rows}") + teacher = saved_sims_for(after, "teacher@phet.test") + j.check("other_account_untouched", teacher is not None and len(teacher) == 4, f"teacher={teacher}") + j.check("catalog_unchanged", catalog_unchanged(init, after) is True, "catalogue tables untouched") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_14 2.py b/sites/phet_simulations/verify/verify_14 2.py new file mode 100644 index 000000000..09402e765 --- /dev/null +++ b/sites/phet_simulations/verify/verify_14 2.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--14. + +Register test_user@phet.test and save Number Pairs to it. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--14', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("answer_names_saved_sim", contains_any(fa, ["Number Pairs"]), + f"the final answer must say which simulation was saved; final={fa!r}") + j.check("nav_register", navigated_to(t, "/register"), "registration page visited") + j.check("nav_number_pairs", navigated_to(t, "/simulation/number-pairs"), "detail page opened") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ci, ca = table_counts(init), table_counts(after) + j.check("exactly_one_new_user", + ci is not None and ca is not None and ca["user"] == ci["user"] + 1, + f"users {ci and ci['user']} -> {ca and ca['user']}") + j.check("new_user_has_the_named_email", user_exists(after, email="test_user@phet.test") is True, + "test_user@phet.test present") + rows = saved_rows_for(after, "test_user@phet.test") + j.check("saved_number_pairs", rows is not None and any(r[0] == "number-pairs" for r in rows), + f"saved={rows}") + j.check("exactly_one_new_save", + ci is not None and ca is not None and ca["saved_simulation"] == ci["saved_simulation"] + 1, + f"saved rows {ci and ci['saved_simulation']} -> {ca and ca['saved_simulation']}") + j.check("catalog_unchanged", catalog_unchanged(init, after) is True, "catalogue tables untouched") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_15 2.py b/sites/phet_simulations/verify/verify_15 2.py new file mode 100644 index 000000000..061710a2b --- /dev/null +++ b/sites/phet_simulations/verify/verify_15 2.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--15. + +Activities filtered to Elementary School: title and duration. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--15', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_activities", navigated_to(t, "/teachers/activities"), "activities list visited") + j.check("used_grade_filter", navigated_to(t, "grade=elementary"), "elementary filter applied") + j.check("answer_title", contains_any(fa, ["Equivalent Fractions Game"]), f"final={fa!r}") + j.check("answer_duration", has_number(fa, 50), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_16 2.py b/sites/phet_simulations/verify/verify_16 2.py new file mode 100644 index 000000000..bae506296 --- /dev/null +++ b/sites/phet_simulations/verify/verify_16 2.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--16. + +About page: the four published totals. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--16', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_about", navigated_to(t, "/about"), "about page visited") + nums = numbers_in(fa) + j.check("answer_simulations", 120 in nums, f"numbers={nums}") + j.check("answer_subjects", 5 in nums, f"numbers={nums}") + j.check("answer_languages", 132 in nums, f"numbers={nums}") + j.check("answer_activities", 14 in nums, f"numbers={nums}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_17 2.py b/sites/phet_simulations/verify/verify_17 2.py new file mode 100644 index 000000000..8c88c5461 --- /dev/null +++ b/sites/phet_simulations/verify/verify_17 2.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--17. + +Compare translation counts of two named simulations. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--17', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_build_an_atom", navigated_to(t, "/simulation/build-an-atom"), "first detail page opened") + j.check("nav_membrane_transport", navigated_to(t, "/simulation/membrane-transport"), "second detail page opened") + j.check("answer_names_winner", contains_any(fa, ["Build an Atom"]), f"final={fa!r}") + j.check("answer_difference", has_number(fa, 73), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_2 2.py b/sites/phet_simulations/verify/verify_2 2.py new file mode 100644 index 000000000..6da8f46e3 --- /dev/null +++ b/sites/phet_simulations/verify/verify_2 2.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--2. + +The only Biology sim not offered at university: report its grade levels. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--2', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_biology_catalog", navigated_any(t, ["subject=biology", "/simulations/category/biology"]), + "biology listing visited") + j.check("nav_natural_selection", navigated_to(t, "/simulation/natural-selection"), "target detail page opened") + j.check("answer_names_target", contains_any(fa, ["Natural Selection"]), f"final={fa!r}") + j.check("answer_grades", contains_all(fa, ["element", "middle", "high"]), f"final={fa!r}") + # The task text itself contains the phrase "not offered at university level", so a + # bare substring test would fail a correct answer that restates the question. Bind to + # the page's own rendering of the band instead, and to claims of targeting it. + low = (fa or "").casefold() + claims_university = ("university (ages 18+)" in low + or "ages 18+" in low + or "targets university" in low + or "including university" in low) + j.check("answer_does_not_claim_university", not claims_university, f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_3 2.py b/sites/phet_simulations/verify/verify_3 2.py new file mode 100644 index 000000000..1d9fdd661 --- /dev/null +++ b/sites/phet_simulations/verify/verify_3 2.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--3. + +New release filter: how many of the New sims were released in 2025. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, counts, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--3', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_new_filter", navigated_to(t, "release=new"), "release=new in a visited URL") + j.check("answer_count_five", counts(fa, 5, "simulation", "sim", "release", "new"), + f"the count must be reported as a number of simulations; numbers={numbers_in(fa)} final={fa!r}") + j.check("answer_not_twelve", not has_number(fa, 12), "12 is the whole New set, not the 2025 subset") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_4 2.py b/sites/phet_simulations/verify/verify_4 2.py new file mode 100644 index 000000000..49c46825f --- /dev/null +++ b/sites/phet_simulations/verify/verify_4 2.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--4. + +Sort by most translated: top simulation and its count. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--4', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_translations_sort", navigated_to(t, "sort=translations"), "sort=translations in a visited URL") + j.check("answer_top_title", contains_any(fa, ["Build an Atom"]), f"final={fa!r}") + j.check("answer_top_count", has_number(fa, 104), f"numbers={numbers_in(fa)}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_5 2.py b/sites/phet_simulations/verify/verify_5 2.py new file mode 100644 index 000000000..856ca5d1e --- /dev/null +++ b/sites/phet_simulations/verify/verify_5 2.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--5. + +Most recently released simulation: date and subjects. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--5', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_catalog", navigated_to(t, "/simulations"), "catalogue visited") + j.check("nav_target", navigated_to(t, "/simulation/quantum-wave-interference"), "target detail page opened") + j.check("answer_title", contains_any(fa, ["Quantum Wave Interference"]), f"final={fa!r}") + j.check("answer_release_date", "2026-09-10" in dates_in(fa), f"dates={dates_in(fa)} final={fa!r}") + j.check("answer_subjects", contains_all(fa, ["physics", "chem"]), f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_6 2.py b/sites/phet_simulations/verify/verify_6 2.py new file mode 100644 index 000000000..fe123fc18 --- /dev/null +++ b/sites/phet_simulations/verify/verify_6 2.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--6. + +Translations page: how many simulations are available in Arabic. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--6', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_translations", navigated_to(t, "/translations"), "translations page visited") + j.check("answer_count", has_number(fa, 119), f"numbers={numbers_in(fa)}") + j.check("answer_not_total", not has_number(fa, 120), "120 is the English/total count, not Arabic") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_7 2.py b/sites/phet_simulations/verify/verify_7 2.py new file mode 100644 index 000000000..47e529cf7 --- /dev/null +++ b/sites/phet_simulations/verify/verify_7 2.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--7. + +Translations page: the Arabic (Morocco) entry count. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--7', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("nav_translations", navigated_to(t, "/translations"), "translations page visited") + j.check("answer_count", has_number(fa, 103), f"numbers={numbers_in(fa)}") + j.check("answer_not_plain_arabic", not has_number(fa, 119), "119 is plain Arabic, not Arabic (Morocco)") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_8 2.py b/sites/phet_simulations/verify/verify_8 2.py new file mode 100644 index 000000000..a91bb6bb3 --- /dev/null +++ b/sites/phet_simulations/verify/verify_8 2.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--8. + +Search 'quantum': result count, then the 2026 release's version. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, counts, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--8', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_search", navigated_to(t, "/search"), "search used") + j.check("answer_result_count", counts(fa, 7, "result", "simulation", "sim", "hit", "match"), + f"the count must be reported as a number of results; numbers={numbers_in(fa)} final={fa!r}") + j.check("nav_target", navigated_to(t, "/simulation/quantum-wave-interference"), "2026 release opened") + j.check("answer_version", contains_all(fa, ["1.0.0"]), f"final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/phet_simulations/verify/verify_9 2.py b/sites/phet_simulations/verify/verify_9 2.py new file mode 100644 index 000000000..40597b9bc --- /dev/null +++ b/sites/phet_simulations/verify/verify_9 2.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Deterministic verifier for PhET task PhET Interactive Simulations--9. + +Heat & Thermo topic filter: how many simulations. + +Ground truth is hardcoded here and nowhere in tasks.jsonl. +Input/Output: see verify_lib.parse_args / Judge.emit. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, navigated_any, final_answer, last_shot, + contains_all, contains_any, answer_equals, numbers_in, has_number, + dates_in, resolve_db, saved_sims_for, saved_rows_for, user_exists, + read_only_run, catalog_unchanged, table_counts, db_query, + llm_text_match, counts, Judge, parse_args) + + +def main(): + a = parse_args() + j = Judge('PhET Interactive Simulations--9', a.no_llm) + t = load_run(a.run_dir) + fa = final_answer(t) + j.check("final_answer_nonempty", bool(fa), f"final={fa!r}") + j.check("used_topic_filter", navigated_to(t, "topic=heat-and-thermo"), "topic facet used") + j.check("answer_count", counts(fa, 9, "simulation", "sim", "result"), + f"the count must be reported as a number of simulations; numbers={numbers_in(fa)} final={fa!r}") + j.check("answer_names_topic", contains_any(fa, ["Heat", "Thermo"]), + f"the answer must say which topic was counted; final={fa!r}") + init = resolve_db(a.initial_db, a.container, "instance_seed") + after = resolve_db(a.after_db, a.container, "instance") + ro = read_only_run(init, after) + j.check("run_is_read_only", ro is True, f"read_only={ro}") + j.emit() + + +if __name__ == "__main__": + main()