{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Colab Fit Diagnostic Notebook\n",
    "\n",
    "Diagnoses why Fit doesn't start on Google Colab.\n",
    "\n",
    "Run each cell **in order** on Colab and share the output."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 0. Install"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install -q lizyml-widget lizyml[plots,tuning,calibration,explain]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Environment Info"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys, platform\n",
    "print(f\"Python: {sys.version}\")\n",
    "print(f\"Platform: {platform.platform()}\")\n",
    "\n",
    "import anywidget, ipywidgets, traitlets\n",
    "print(f\"anywidget: {anywidget.__version__}\")\n",
    "print(f\"ipywidgets: {ipywidgets.__version__}\")\n",
    "print(f\"traitlets: {traitlets.__version__}\")\n",
    "\n",
    "import lizyml_widget\n",
    "print(f\"lizyml_widget: {lizyml_widget.__version__}\")\n",
    "\n",
    "try:\n",
    "    import google.colab\n",
    "    print(\"google.colab: available\")\n",
    "except ImportError:\n",
    "    print(\"google.colab: NOT available\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. JS Colab Detection Test\n",
    "\n",
    "This is the **critical test**. It checks whether our `isColab()` JS function returns `true`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import display, Javascript\n",
    "\n",
    "display(Javascript(\"\"\"\n",
    "(function() {\n",
    "    const results = {};\n",
    "\n",
    "    // Test 1: Current detection method\n",
    "    results.linkHrefColab = document.querySelector('link[href*=\"colab\"]') !== null;\n",
    "\n",
    "    // Test 2: window.google.colab\n",
    "    results.windowGoogleColab = !!(window.google && window.google.colab);\n",
    "\n",
    "    // Test 3: colab-specific elements\n",
    "    results.colabNotebookEl = document.querySelector('colab-notebook') !== null;\n",
    "    results.colabToolbarEl = document.querySelector('colab-toolbar') !== null;\n",
    "\n",
    "    // Test 4: body classes or IDs\n",
    "    results.bodyId = document.body?.id || '(none)';\n",
    "    results.bodyClasses = document.body?.className || '(none)';\n",
    "\n",
    "    // Test 5: all <link> tags (href contains what?)\n",
    "    const links = Array.from(document.querySelectorAll('link[rel=\"stylesheet\"]'));\n",
    "    results.stylesheetHrefs = links.map(l => l.href).filter(h => h);\n",
    "\n",
    "    // Test 6: meta tags\n",
    "    const metas = Array.from(document.querySelectorAll('meta'));\n",
    "    results.metaTags = metas.map(m => ({name: m.name, content: m.content})).filter(m => m.name);\n",
    "\n",
    "    // Test 7: user agent\n",
    "    results.userAgent = navigator.userAgent;\n",
    "\n",
    "    // Output\n",
    "    const el = document.createElement('pre');\n",
    "    el.id = 'colab-diag-results';\n",
    "    el.style.cssText = 'background:#1e1e1e;color:#0f0;padding:12px;border-radius:8px;font-size:13px;max-height:600px;overflow:auto;';\n",
    "    el.textContent = JSON.stringify(results, null, 2);\n",
    "    element.appendChild(el);\n",
    "})();\n",
    "\"\"\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. BG Thread → JS Communication Test\n",
    "\n",
    "Tests whether traitlet writes from a background thread reach JS on Colab."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import anywidget\n",
    "import traitlets\n",
    "import threading\n",
    "import time\n",
    "\n",
    "class DiagWidget(anywidget.AnyWidget):\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model, el }) {\n",
    "        const log = [];\n",
    "        function addLog(msg) {\n",
    "            log.push(`[${new Date().toISOString().slice(11,19)}] ${msg}`);\n",
    "            el.innerHTML = `<pre style=\"background:#1e1e1e;color:#0f0;padding:12px;border-radius:8px;font-size:13px;max-height:400px;overflow:auto;\">${log.join('\\\\n')}</pre>`;\n",
    "        }\n",
    "\n",
    "        // isColab check (same as production code)\n",
    "        const linkColab = document.querySelector('link[href*=\"colab\"]') !== null;\n",
    "        const googleColab = !!(window.google && window.google.colab);\n",
    "        addLog(`isColab detection: link[href*=colab]=${linkColab}, window.google.colab=${googleColab}`);\n",
    "\n",
    "        // Listen for traitlet changes\n",
    "        let traitletReceived = false;\n",
    "        model.on('change:counter', () => {\n",
    "            traitletReceived = true;\n",
    "            addLog(`TRAITLET change:counter = ${model.get('counter')}`);\n",
    "        });\n",
    "\n",
    "        // Listen for msg:custom\n",
    "        let msgReceived = false;\n",
    "        model.on('msg:custom', (msg) => {\n",
    "            msgReceived = true;\n",
    "            addLog(`MSG:CUSTOM received: ${JSON.stringify(msg)}`);\n",
    "        });\n",
    "\n",
    "        // After 5 seconds, report what was received\n",
    "        setTimeout(() => {\n",
    "            addLog('--- 5s Summary ---');\n",
    "            addLog(`Traitlet from BG thread received: ${traitletReceived}`);\n",
    "            addLog(`msg:custom from BG thread received: ${msgReceived}`);\n",
    "            if (!traitletReceived && !msgReceived) {\n",
    "                addLog('DIAGNOSIS: BG thread comm is BLOCKED (expected on Colab)');\n",
    "            } else {\n",
    "                addLog('DIAGNOSIS: BG thread comm is WORKING');\n",
    "            }\n",
    "        }, 5000);\n",
    "\n",
    "        addLog('Waiting for BG thread writes...');\n",
    "    }\n",
    "    \"\"\"\n",
    "    counter = traitlets.Int(0).tag(sync=True)\n",
    "\n",
    "    def start_bg_test(self):\n",
    "        \"\"\"Start a background thread that writes to traitlets and sends custom msgs.\"\"\"\n",
    "        def worker():\n",
    "            time.sleep(1)\n",
    "            # Write traitlet from BG thread\n",
    "            self.counter = 1\n",
    "            time.sleep(0.5)\n",
    "            # Send custom message from BG thread\n",
    "            try:\n",
    "                self.send({\"type\": \"bg_test\", \"value\": 42})\n",
    "            except Exception as e:\n",
    "                print(f\"send() from BG thread failed: {e}\")\n",
    "            time.sleep(0.5)\n",
    "            self.counter = 2\n",
    "        t = threading.Thread(target=worker, daemon=True)\n",
    "        t.start()\n",
    "        print(\"BG thread started. Watch the widget output above.\")\n",
    "\n",
    "w = DiagWidget()\n",
    "display(w)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Run this AFTER the widget above is displayed\n",
    "w.start_bg_test()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Poll Mechanism Test\n",
    "\n",
    "Tests whether JS → Python → JS custom message round-trip works (the polling fallback)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PollDiagWidget(anywidget.AnyWidget):\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model, el }) {\n",
    "        const log = [];\n",
    "        function addLog(msg) {\n",
    "            log.push(`[${new Date().toISOString().slice(11,19)}] ${msg}`);\n",
    "            el.innerHTML = `<pre style=\"background:#1e1e1e;color:#0f0;padding:12px;border-radius:8px;font-size:13px;max-height:400px;overflow:auto;\">${log.join('\\\\n')}</pre>`;\n",
    "        }\n",
    "\n",
    "        // Listen for reply\n",
    "        model.on('msg:custom', (msg) => {\n",
    "            addLog(`REPLY received: ${JSON.stringify(msg)}`);\n",
    "        });\n",
    "\n",
    "        // Send poll request\n",
    "        addLog('Sending poll request via model.send()...');\n",
    "        model.send({ type: 'poll' });\n",
    "\n",
    "        // Send another after 2s\n",
    "        setTimeout(() => {\n",
    "            addLog('Sending 2nd poll request...');\n",
    "            model.send({ type: 'poll' });\n",
    "        }, 2000);\n",
    "\n",
    "        setTimeout(() => {\n",
    "            if (log.length <= 3) {\n",
    "                addLog('DIAGNOSIS: Poll round-trip FAILED — model.send() or self.send() broken');\n",
    "            } else {\n",
    "                addLog('DIAGNOSIS: Poll round-trip WORKING');\n",
    "            }\n",
    "        }, 5000);\n",
    "    }\n",
    "    \"\"\"\n",
    "    poll_count = traitlets.Int(0).tag(sync=True)\n",
    "\n",
    "    def _handle_custom_msg(self, content, buffers):\n",
    "        if content.get('type') == 'poll':\n",
    "            self.poll_count += 1\n",
    "            self.send({'type': 'poll_reply', 'count': self.poll_count})\n",
    "        else:\n",
    "            super()._handle_custom_msg(content, buffers)\n",
    "\n",
    "pw = PollDiagWidget()\n",
    "display(pw)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Actual LizyWidget Fit Test\n",
    "\n",
    "Minimal Fit test with logging."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "logging.basicConfig(level=logging.DEBUG)\n",
    "logging.getLogger('lizyml_widget').setLevel(logging.DEBUG)\n",
    "\n",
    "import pandas as pd\n",
    "from sklearn.datasets import load_iris\n",
    "from lizyml_widget import LizyWidget\n",
    "\n",
    "iris = load_iris(as_frame=True)\n",
    "df = pd.concat([iris.data, iris.target.rename('target')], axis=1)\n",
    "print(f\"Data shape: {df.shape}\")\n",
    "print(f\"Columns: {list(df.columns)}\")\n",
    "\n",
    "w = LizyWidget()\n",
    "w.load(df, target='target')\n",
    "print(f\"Status after load: {w.status}\")\n",
    "print(f\"Config: {dict(w.config)}\")\n",
    "display(w)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# After clicking Fit in the widget UI above, run this cell to check Python-side state:\n",
    "import time\n",
    "print(f\"Status: {w.status}\")\n",
    "print(f\"Job type: {w.job_type}\")\n",
    "print(f\"Progress: {dict(w.progress)}\")\n",
    "print(f\"Elapsed: {w.elapsed_sec}\")\n",
    "print(f\"Error: {dict(w.error)}\")\n",
    "if w._job_thread:\n",
    "    print(f\"Job thread alive: {w._job_thread.is_alive()}\")\n",
    "else:\n",
    "    print(\"Job thread: None (never started)\")\n",
    "print(f\"Fit summary: {dict(w.fit_summary)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Direct Python Fit (bypass widget)\n",
    "\n",
    "Verifies that the ML backend itself works on Colab."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Direct LizyML fit (no widget, no threads)\n",
    "try:\n",
    "    import lizyml\n",
    "    model = lizyml.LizyModel()\n",
    "    model.fit(df.drop('target', axis=1), df['target'])\n",
    "    print(f\"Direct fit SUCCESS. Score: {model.score(df.drop('target', axis=1), df['target']):.4f}\")\n",
    "except Exception as e:\n",
    "    print(f\"Direct fit FAILED: {e}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}