{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# GO-CAM AI Demo with Noctua Screenshots\n",
    "\n",
    "This notebook demonstrates GO-CAM manipulation with automatic screenshot capture of the Noctua web interface.\n",
    "\n",
    "## Prerequisites\n",
    "\n",
    "1. Chrome or Firefox browser installed\n",
    "2. ChromeDriver or GeckoDriver installed:\n",
    "   ```bash\n",
    "   # macOS with Homebrew\n",
    "   brew install chromedriver\n",
    "   # or\n",
    "   brew install geckodriver\n",
    "   ```\n",
    "\n",
    "3. Required Python packages:\n",
    "   ```bash\n",
    "   uv add selenium pillow\n",
    "   ```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from pathlib import Path\n",
    "from IPython.display import Image, display, HTML\n",
    "\n",
    "from noctua.barista import BaristaClient\n",
    "from noctua.noctua_screenshots import NoctuaScreenshotCapture\n",
    "\n",
    "# Ensure BARISTA_TOKEN is set in your environment\n",
    "# For development: Contact the GO team for a dev token  \n",
    "# For production: Get token from Noctua login\n",
    "if not os.environ.get(\"BARISTA_TOKEN\"):\n",
    "    print(\"ERROR: BARISTA_TOKEN environment variable not set\")\n",
    "    print(\"Please set: export BARISTA_TOKEN=your-token-here\")\n",
    "    raise SystemExit(1)\n",
    "\n",
    "# Create screenshots directory\n",
    "screenshots_dir = Path(\"screenshots\")\n",
    "screenshots_dir.mkdir(exist_ok=True)\n",
    "\n",
    "print(\"Environment configured\")\n",
    "print(f\"Screenshots will be saved to: {screenshots_dir.absolute()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create Model and Initialize Screenshot Capture\n",
    "\n",
    "We'll create a new model and open it in Noctua for visual tracking."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Created model: gomodel:68d6f96e00000294\n",
      "\n",
      "Noctua URL: http://noctua-dev.berkeleybop.org/editor/graph/gomodel:68d6f96e00000294?barista_token=0wikitmf1vch103exckj\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<a href=\"http://noctua-dev.berkeleybop.org/editor/graph/gomodel:68d6f96e00000294?barista_token=0wikitmf1vch103exckj\" target=\"_blank\">Open in Noctua</a>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Create Barista client\n",
    "client = BaristaClient()\n",
    "\n",
    "# Create a new model\n",
    "response = client.create_model(title=\"Visual Demo: Signal Transduction Pathway\")\n",
    "if not response.ok:\n",
    "    print(f\"Failed to create model: {response.raw}\")\n",
    "    raise Exception(\"Could not create model\")\n",
    "\n",
    "model_id = response.model_id\n",
    "print(f\"Created model: {model_id}\")\n",
    "\n",
    "# Get the Noctua URL with token\n",
    "token = os.environ.get(\"BARISTA_TOKEN\", \"\")\n",
    "noctua_url = f\"http://noctua-dev.berkeleybop.org/editor/graph/{model_id}\"\n",
    "if token:\n",
    "    noctua_url += f\"?barista_token={token}\"\n",
    "print(f\"\\nNoctua URL: {noctua_url}\")\n",
    "display(HTML(f'<a href=\"{noctua_url}\" target=\"_blank\">Open in Noctua</a>'))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Initialize Screenshot Capture\n",
    "\n",
    "This will open a browser window to track changes. Set `headless=True` to run without GUI."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Error sending stats to Plausible: error sending request for url (https://plausible.io/api/event)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Browser started successfully\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Timeout waiting for Noctua editor to load\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model opened in browser\n",
      "Initial screenshot saved\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 76648 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Initialize screenshot capture\n",
    "# Note: Set headless=True to run without opening a browser window\n",
    "capture = NoctuaScreenshotCapture(\n",
    "    headless=False,  # Set to True for headless mode\n",
    "    screenshot_dir=\"screenshots\",\n",
    "    dev_mode=True\n",
    ")\n",
    "\n",
    "try:\n",
    "    capture.start()\n",
    "    print(\"Browser started successfully\")\n",
    "    \n",
    "    # Open the model in Noctua\n",
    "    if capture.open_model(model_id, wait_time=8):\n",
    "        print(\"Model opened in browser\")\n",
    "        \n",
    "        # Take initial screenshot\n",
    "        initial_screenshot = capture.capture(\n",
    "            filename=\"01_empty_model.png\",\n",
    "            description=\"Empty model\"\n",
    "        )\n",
    "        \n",
    "        if initial_screenshot:\n",
    "            print(\"Initial screenshot saved\")\n",
    "            display(Image(initial_screenshot, width=800))\n",
    "    else:\n",
    "        print(\"Could not open model in browser\")\n",
    "        print(\"Make sure Chrome/Firefox and appropriate driver are installed\")\n",
    "        \n",
    "except Exception as e:\n",
    "    print(f\"Error setting up browser: {e}\")\n",
    "    print(\"Continuing without screenshots...\")\n",
    "    capture.close()\n",
    "    capture = None"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1: Add Molecular Functions\n",
    "\n",
    "Add molecular function nodes and capture screenshots after each addition."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding receptor activity...\n",
      "Added receptor: gomodel:68d6f96e00000294/68d6f96e00000295\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 88344 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Store individual IDs for later use\n",
    "individuals = {}\n",
    "\n",
    "# Add receptor activity\n",
    "print(\"Adding receptor activity...\")\n",
    "response = client.add_individual(\n",
    "    model_id,\n",
    "    \"GO:0004888\",  # transmembrane signaling receptor activity\n",
    "    assign_var=\"receptor\"\n",
    ")\n",
    "\n",
    "if response.ok:\n",
    "    # Find the added individual\n",
    "    for ind in response.individuals:\n",
    "        types = ind.get(\"type\", [])\n",
    "        for t in types:\n",
    "            if t.get(\"id\") == \"GO:0004888\":\n",
    "                individuals[\"receptor\"] = ind[\"id\"]\n",
    "                print(f\"Added receptor: {ind['id']}\")\n",
    "                break\n",
    "    \n",
    "    # Refresh and screenshot\n",
    "    if capture:\n",
    "        capture.refresh_model(wait_time=3)\n",
    "        screenshot = capture.capture(\n",
    "            filename=\"02_receptor_added.png\",\n",
    "            description=\"After adding receptor\"\n",
    "        )\n",
    "        if screenshot:\n",
    "            display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding GTPase activity...\n",
      "Added GTPase: gomodel:68d6f96e00000294/68d6f96e00000296\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 91412 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Add GTPase activity\n",
    "print(\"Adding GTPase activity...\")\n",
    "response = client.add_individual(\n",
    "    model_id,\n",
    "    \"GO:0003924\",  # GTPase activity\n",
    "    assign_var=\"gtpase\"\n",
    ")\n",
    "\n",
    "if response.ok:\n",
    "    for ind in response.individuals:\n",
    "        types = ind.get(\"type\", [])\n",
    "        for t in types:\n",
    "            if t.get(\"id\") == \"GO:0003924\":\n",
    "                individuals[\"gtpase\"] = ind[\"id\"]\n",
    "                print(f\"Added GTPase: {ind['id']}\")\n",
    "                break\n",
    "    \n",
    "    if capture:\n",
    "        capture.refresh_model(wait_time=3)\n",
    "        screenshot = capture.capture(\n",
    "            filename=\"03_gtpase_added.png\",\n",
    "            description=\"After adding GTPase\"\n",
    "        )\n",
    "        if screenshot:\n",
    "            display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding kinase activity...\n",
      "Added kinase: gomodel:68d6f96e00000294/68d6f96e00000297\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 97508 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Add kinase activity\n",
    "print(\"Adding kinase activity...\")\n",
    "response = client.add_individual(\n",
    "    model_id,\n",
    "    \"GO:0004674\",  # protein serine/threonine kinase activity\n",
    "    assign_var=\"kinase\"\n",
    ")\n",
    "\n",
    "if response.ok:\n",
    "    for ind in response.individuals:\n",
    "        types = ind.get(\"type\", [])\n",
    "        for t in types:\n",
    "            if t.get(\"id\") == \"GO:0004674\":\n",
    "                individuals[\"kinase\"] = ind[\"id\"]\n",
    "                print(f\"Added kinase: {ind['id']}\")\n",
    "                break\n",
    "    \n",
    "    if capture:\n",
    "        capture.refresh_model(wait_time=3)\n",
    "        screenshot = capture.capture(\n",
    "            filename=\"04_kinase_added.png\",\n",
    "            description=\"After adding kinase\"\n",
    "        )\n",
    "        if screenshot:\n",
    "            display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2: Add Causal Relationships\n",
    "\n",
    "Connect the molecular functions with regulatory relationships."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding edge: Receptor -> GTPase\n",
      "Edge added successfully\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 113656 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Add edge: Receptor -> GTPase\n",
    "if \"receptor\" in individuals and \"gtpase\" in individuals:\n",
    "    print(\"Adding edge: Receptor -> GTPase\")\n",
    "    response = client.add_fact(\n",
    "        model_id,\n",
    "        subject_id=individuals[\"receptor\"],\n",
    "        object_id=individuals[\"gtpase\"],\n",
    "        predicate_id=\"RO:0002413\"  # directly positively regulates\n",
    "    )\n",
    "    \n",
    "    if response.ok:\n",
    "        print(\"Edge added successfully\")\n",
    "        \n",
    "        if capture:\n",
    "            capture.refresh_model(wait_time=3)\n",
    "            screenshot = capture.capture(\n",
    "                filename=\"05_receptor_to_gtpase.png\",\n",
    "                description=\"After adding Receptor->GTPase edge\"\n",
    "            )\n",
    "            if screenshot:\n",
    "                display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding edge: GTPase -> Kinase\n",
      "Edge added successfully\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 112244 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Add edge: GTPase -> Kinase\n",
    "if \"gtpase\" in individuals and \"kinase\" in individuals:\n",
    "    print(\"Adding edge: GTPase -> Kinase\")\n",
    "    response = client.add_fact(\n",
    "        model_id,\n",
    "        subject_id=individuals[\"gtpase\"],\n",
    "        object_id=individuals[\"kinase\"],\n",
    "        predicate_id=\"RO:0002413\"  # directly positively regulates\n",
    "    )\n",
    "    \n",
    "    if response.ok:\n",
    "        print(\"Edge added successfully\")\n",
    "        \n",
    "        if capture:\n",
    "            capture.refresh_model(wait_time=3)\n",
    "            screenshot = capture.capture(\n",
    "                filename=\"06_gtpase_to_kinase.png\",\n",
    "                description=\"After adding GTPase->Kinase edge\"\n",
    "            )\n",
    "            if screenshot:\n",
    "                display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 3: Add Evidence\n",
    "\n",
    "Add evidence annotation to support the relationships."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding evidence to GTPase->Kinase edge...\n",
      "Evidence added successfully\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 112296 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Add evidence to GTPase->Kinase edge\n",
    "if \"gtpase\" in individuals and \"kinase\" in individuals:\n",
    "    print(\"Adding evidence to GTPase->Kinase edge...\")\n",
    "    \n",
    "    evidence_requests = client.req_add_evidence_to_fact(\n",
    "        model_id,\n",
    "        subject_id=individuals[\"gtpase\"],\n",
    "        object_id=individuals[\"kinase\"],\n",
    "        predicate_id=\"RO:0002413\",\n",
    "        eco_id=\"ECO:0000314\",  # direct assay evidence\n",
    "        sources=[\"PMID:12345678\", \"PMID:87654321\"],\n",
    "        with_from=[\"UniProtKB:P01112\"]  # HRAS\n",
    "    )\n",
    "    \n",
    "    response = client.m3_batch(evidence_requests)\n",
    "    \n",
    "    if response.ok:\n",
    "        print(\"Evidence added successfully\")\n",
    "        \n",
    "        if capture:\n",
    "            capture.refresh_model(wait_time=3)\n",
    "            screenshot = capture.capture(\n",
    "                filename=\"07_evidence_added.png\",\n",
    "                description=\"After adding evidence\"\n",
    "            )\n",
    "            if screenshot:\n",
    "                display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 4: View Complete Model\n",
    "\n",
    "Final screenshot of the complete model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Final model state:\n",
      "- Model ID: gomodel:68d6f96e00000294\n",
      "- Individuals: 4\n",
      "- Facts: 2\n",
      "\n",
      "Final model visualization:\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 116888 characters]",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {
      "image/png": {
       "width": 800
      }
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Get final model state\n",
    "response = client.get_model(model_id)\n",
    "\n",
    "if response.ok:\n",
    "    print(\"Final model state:\")\n",
    "    print(f\"- Model ID: {response.model_id}\")\n",
    "    print(f\"- Individuals: {len(response.individuals)}\")\n",
    "    print(f\"- Facts: {len(response.facts)}\")\n",
    "    \n",
    "    if capture:\n",
    "        # Take a final screenshot\n",
    "        screenshot = capture.capture(\n",
    "            filename=\"08_complete_model.png\",\n",
    "            description=\"Complete model\"\n",
    "        )\n",
    "        if screenshot:\n",
    "            print(\"\\nFinal model visualization:\")\n",
    "            display(Image(screenshot, width=800))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Generate Screenshot Gallery\n",
    "\n",
    "Create an HTML gallery of all screenshots taken during the session."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<h3>Screenshot Gallery</h3>\n",
       "<div style='display: flex; flex-wrap: wrap;'>\n",
       "\n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/01_empty_model.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>01_empty_model.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/02_receptor_added.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>02_receptor_added.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/03_gtpase_added.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>03_gtpase_added.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/04_kinase_added.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>04_kinase_added.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/05_receptor_to_gtpase.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>05_receptor_to_gtpase.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/06_gtpase_to_kinase.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>06_gtpase_to_kinase.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/07_evidence_added.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>07_evidence_added.png</p>\n",
       "        </div>\n",
       "        \n",
       "        <div style='margin: 10px; text-align: center;'>\n",
       "            <img src='screenshots/08_complete_model.png' width='300' style='border: 1px solid #ddd;'/>\n",
       "            <p style='font-size: 12px;'>08_complete_model.png</p>\n",
       "        </div>\n",
       "        </div>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Generate gallery of all screenshots\n",
    "from pathlib import Path\n",
    "\n",
    "screenshot_files = sorted(Path(\"screenshots\").glob(\"*.png\"))\n",
    "\n",
    "if screenshot_files:\n",
    "    html_content = \"<h3>Screenshot Gallery</h3>\\n\"\n",
    "    html_content += \"<div style='display: flex; flex-wrap: wrap;'>\\n\"\n",
    "    \n",
    "    for img_path in screenshot_files:\n",
    "        html_content += f'''\n",
    "        <div style='margin: 10px; text-align: center;'>\n",
    "            <img src='{img_path}' width='300' style='border: 1px solid #ddd;'/>\n",
    "            <p style='font-size: 12px;'>{img_path.name}</p>\n",
    "        </div>\n",
    "        '''\n",
    "    \n",
    "    html_content += \"</div>\\n\"\n",
    "    display(HTML(html_content))\n",
    "else:\n",
    "    print(\"No screenshots found\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cleanup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Browser closed\n",
      "\n",
      "Model URL: http://noctua-dev.berkeleybop.org/editor/graph/gomodel:68d6f96e00000294?barista_token=0wikitmf1vch103exckj\n",
      "Screenshots saved in: /Users/cjm/repos/noctua-py/docs/examples/screenshots\n"
     ]
    }
   ],
   "source": [
    "# Close the browser\n",
    "if capture:\n",
    "    capture.close()\n",
    "    print(\"Browser closed\")\n",
    "\n",
    "print(f\"\\nModel URL: {noctua_url}\")\n",
    "print(f\"Screenshots saved in: {screenshots_dir.absolute()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "This notebook demonstrated:\n",
    "\n",
    "1. **Automated Browser Control**: Using Selenium to open and control Noctua\n",
    "2. **Visual Documentation**: Capturing screenshots at each step of model construction\n",
    "3. **API + UI Sync**: Showing how API changes appear in the Noctua interface\n",
    "4. **Complete Workflow**: From empty model to fully annotated pathway with evidence\n",
    "\n",
    "The screenshots provide visual documentation of the model building process, useful for:\n",
    "- Training materials\n",
    "- Documentation\n",
    "- Debugging model construction\n",
    "- Quality assurance\n",
    "\n",
    "### Tips\n",
    "\n",
    "- Use `headless=True` for automated pipelines\n",
    "- Adjust wait times based on network speed\n",
    "- Screenshots are automatically saved with descriptive names\n",
    "- The browser window can be controlled programmatically for zooming, panning, etc."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}