// bugs.jsx — workspace-shared bug reports.
//
// Surface 1 — floating "🐞 Report bug" button: pinned bottom-right on
//             every internal page. Click → BugReportModal.
// Surface 2 — full BugsView page (sidebar entry under Admin).
//
// Capture options inside the modal:
//   - Take screenshot   → navigator.mediaDevices.getDisplayMedia({ video })
//                         draws one video frame onto a canvas, exports PNG.
//   - Record screen     → reuses the same getDisplayMedia + MediaRecorder
//                         pattern as the support portal recorder.
//   - Attach file       → standard <input type="file"> for log files /
//                         pasted images.
// Both screen-capture options need the user to grant the browser screen-
// share permission once per recording (browser-enforced, not ours).

const BUG_SEVERITIES = [
  { id: "low",      label: "Low",      color: "#579bfc" },
  { id: "normal",   label: "Normal",   color: "#9aa0ae" },
  { id: "high",     label: "High",     color: "#fdab3d" },
  { id: "critical", label: "Critical", color: "#e2445c" },
];
const BUG_STATUSES = [
  { id: "open",          label: "Open",          color: "#579bfc" },
  { id: "investigating", label: "Investigating", color: "#fdab3d" },
  { id: "fixed",         label: "Fixed",         color: "#00c875" },
  { id: "wont_fix",      label: "Won't fix",     color: "#9aa0ae" },
];

function _bugSev(id)    { return BUG_SEVERITIES.find(s => s.id === id) || BUG_SEVERITIES[1]; }
function _bugStat(id)   { return BUG_STATUSES.find(s => s.id === id) || BUG_STATUSES[0]; }
function _bugRel(iso)   { return typeof fmtRelative === "function" ? fmtRelative(iso) : (iso || ""); }

// ── Floating "Report bug" button ────────────────────────────────────
function BugFloatingButton({ onOpen }) {
  // Stacked above the chat bubble (which sits at bottom:22 right:22).
  // The .bug-floating-btn class lets chat.css hide this button when
  // the chat panel is open, so the panel never overlaps it.
  return (
    <button onClick={onOpen}
            className="bug-floating-btn"
            title="Report a bug — visible to everyone in this workspace"
            style={{
              position: "fixed", bottom: 88, right: 22, zIndex: 75,
              display: "inline-flex", alignItems: "center", gap: 8,
              padding: "10px 16px", borderRadius: 999,
              background: "var(--ink-strong)", color: "white",
              border: "none", cursor: "pointer",
              fontSize: 13, fontWeight: 600,
              boxShadow: "0 8px 24px rgba(15,23,42,.22), 0 2px 6px rgba(15,23,42,.16)",
              transition: "transform .12s ease, box-shadow .12s ease, opacity .15s ease",
            }}
            onMouseEnter={e => { e.currentTarget.style.transform = "translateY(-1px)"; }}
            onMouseLeave={e => { e.currentTarget.style.transform = "translateY(0)"; }}>
      <span style={{ fontSize: 16 }}>🐞</span> Report bug
    </button>
  );
}

// ── Report modal ────────────────────────────────────────────────────
function BugReportModal({ open, onClose, onCreated }) {
  const [title, setTitle]       = React.useState("");
  // body is HTML now (rich text editor produces inline tags). Stays
  // backwards-compatible — old plain-text bodies render fine because
  // BugBody falls back to whitespace-pre-wrap when no HTML tags exist.
  const [body, setBody]         = React.useState("");
  const [severity, setSeverity] = React.useState("normal");
  const [files, setFiles]       = React.useState([]);   // [{file, kind}]
  const [recording, setRecording] = React.useState(false);
  const [recDur, setRecDur]     = React.useState(0);
  const [busy, setBusy]         = React.useState(false);
  const [err, setErr]           = React.useState("");
  const recRef    = React.useRef(null);
  const recTimer  = React.useRef(null);
  // Track whether the picker is mid-flight. While this is true any
  // close attempt is silently absorbed — getDisplayMedia briefly
  // shifts focus to the OS-level share dialog and on some platforms
  // a phantom mouse event lands on the page when it dismisses, which
  // was tearing the modal down BEFORE the recorder could be wired up.
  const pickerBusy = React.useRef(false);

  if (!open) return null;

  // Centralised close path so every escape route (backdrop, X button,
  // ESC key, Cancel button) gets the same guards. The single most
  // important rule: if a recording is in flight, we do NOT silently
  // unmount — that orphans the MediaRecorder with no UI to stop it.
  function safeClose() {
    if (pickerBusy.current) return;            // share-picker in flight
    if (recording || (recRef.current && recRef.current.state === "recording")) {
      const ok = window.confirm("You're recording. Stop and discard the clip?");
      if (!ok) return;
      try { recRef.current && recRef.current.stop(); } catch {}
      if (recTimer.current) { clearInterval(recTimer.current); recTimer.current = null; }
      setRecording(false);
      // Drop any partial-blob that landed before stop fully resolved.
      // The clip is gone; anything else (title/body/files) is kept.
    } else if (title.trim() || _stripHTML(body).trim() || files.length) {
      const ok = window.confirm("Discard your in-progress bug report?");
      if (!ok) return;
    }
    onClose && onClose();
  }

  // ESC behavior depends on context:
  //   • Recording in flight → stop the recording (KEEP the clip).
  //     The pill UI is dismissed because `recording` flips to false,
  //     and the modal pops back so the user can finalise the report.
  //   • Otherwise            → safeClose() with the usual guards.
  // Bound on the document with capture so the editor's own keydown
  // handler doesn't swallow the escape.
  React.useEffect(() => {
    function onKey(e) {
      if (e.key !== "Escape") return;
      e.stopPropagation();
      if (recording || (recRef.current && recRef.current.state === "recording")) {
        stopRecording();   // graceful: clip is preserved, modal returns
      } else {
        safeClose();
      }
    }
    document.addEventListener("keydown", onKey, true);
    return () => document.removeEventListener("keydown", onKey, true);
  });

  function addFile(file) {
    if (!file) return;
    const kind = (file.type || "").startsWith("image/") ? "image"
              : (file.type || "").startsWith("video/") ? "video" : "file";
    setFiles(prev => [...prev, { file, kind }]);
  }
  function removeFile(idx) { setFiles(prev => prev.filter((_, i) => i !== idx)); }

  // Take a single-frame screenshot of whatever the user shares.
  // We bias the picker toward THIS tab — the bug report is about
  // ZeroProject itself, so 99% of the time the user wants to capture
  // the current tab, not a separate window or the whole screen.
  // See _captureConstraints below for the full rationale.
  async function takeScreenshot() {
    setErr("");
    if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
      setErr("Your browser doesn't support screen capture.");
      return;
    }
    let stream;
    pickerBusy.current = true;
    try { stream = await navigator.mediaDevices.getDisplayMedia(_captureConstraints({ frameRate: false })); }
    catch (e) {
      pickerBusy.current = false;
      if (e && e.name === "NotAllowedError") return;     // user dismissed prompt
      // Some older browsers reject the non-standard preferCurrentTab
      // option with TypeError. Retry with the legacy plain-object
      // form so the feature still works there.
      if (e && (e.name === "TypeError" || /preferCurrentTab|selfBrowser|displaySurface|surfaceSwitching/i.test(e.message || ""))) {
        try { stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false }); }
        catch (e2) {
          if (e2 && e2.name === "NotAllowedError") return;
          setErr("Couldn't start screen capture: " + (e2.message || e2.name));
          return;
        }
      } else {
        setErr("Couldn't start screen capture: " + (e.message || e.name));
        return;
      }
    }
    // Hold the close-guard for one tick AFTER the picker dismisses so
    // any phantom click that lands on the page (Linux/Wayland is the
    // worst offender) doesn't tear the modal down while we're still
    // wiring up the canvas.
    setTimeout(() => { pickerBusy.current = false; }, 350);
    try {
      const video = document.createElement("video");
      video.srcObject = stream;
      video.muted = true;
      await video.play();
      // Give the first frame a tick to populate.
      await new Promise(r => setTimeout(r, 250));
      const w = video.videoWidth, h = video.videoHeight;
      const canvas = document.createElement("canvas");
      canvas.width = w; canvas.height = h;
      canvas.getContext("2d").drawImage(video, 0, 0, w, h);
      const blob = await new Promise(r => canvas.toBlob(r, "image/png"));
      const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
      const f = new File([blob], `screenshot-${stamp}.png`, { type: "image/png" });
      addFile(f);
    } finally {
      try { stream.getTracks().forEach(t => t.stop()); } catch {}
    }
  }

  // Record a clip via MediaRecorder. Same tab-first picker bias as
  // takeScreenshot — the bug report is about ZeroProject so the
  // user almost always wants to capture this tab. Falls back to the
  // legacy plain-object form for browsers that don't support the
  // newer constraint flags (Firefox, Safari).
  async function startRecording() {
    setErr("");
    if (!navigator.mediaDevices || !window.MediaRecorder) {
      setErr("Your browser doesn't support screen recording."); return;
    }
    let stream;
    pickerBusy.current = true;
    try { stream = await navigator.mediaDevices.getDisplayMedia(_captureConstraints({ frameRate: 15 })); }
    catch (e) {
      pickerBusy.current = false;
      if (e && e.name === "NotAllowedError") return;
      if (e && (e.name === "TypeError" || /preferCurrentTab|selfBrowser|displaySurface|surfaceSwitching/i.test(e.message || ""))) {
        try { stream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 15 }, audio: false }); }
        catch (e2) {
          if (e2 && e2.name === "NotAllowedError") return;
          setErr("Couldn't start recording: " + (e2.message || e2.name));
          return;
        }
      } else {
        setErr("Couldn't start recording: " + (e.message || e.name));
        return;
      }
    }
    // Same dismiss guard as takeScreenshot — keep the modal alive for
    // one tick past the picker dismissal so a stray click doesn't
    // close the modal while the recorder is being wired up.
    setTimeout(() => { pickerBusy.current = false; }, 350);
    const mimeCandidates = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"];
    const mime = mimeCandidates.find(m => MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(m)) || "";
    const rec = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined);
    const chunks = [];
    rec.ondataavailable = (e) => { if (e.data && e.data.size) chunks.push(e.data); };
    rec.onstop = () => {
      const blob = new Blob(chunks, { type: rec.mimeType || "video/webm" });
      const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
      addFile(new File([blob], `screen-${stamp}.webm`, { type: blob.type }));
      try { stream.getTracks().forEach(t => t.stop()); } catch {}
    };
    stream.getVideoTracks().forEach(t => { t.onended = () => stopRecording(); });
    rec.start(1000);
    recRef.current = rec;
    setRecording(true);
    setRecDur(0);
    const startedAt = Date.now();
    recTimer.current = setInterval(() => setRecDur(Math.round((Date.now() - startedAt) / 1000)), 500);
  }
  function stopRecording() {
    try { recRef.current && recRef.current.state === "recording" && recRef.current.stop(); } catch {}
    if (recTimer.current) { clearInterval(recTimer.current); recTimer.current = null; }
    setRecording(false);
  }

  async function submit() {
    setErr("");
    if (!title.trim()) { setErr("Give the bug a one-line title."); return; }
    setBusy(true);
    try {
      // Sanitize the HTML on the way out — strip <script>/<iframe>/
      // event handlers / javascript: URLs. The body might still be
      // plain text from a paste; that's fine, sanitize_html is a
      // no-op when there's nothing to strip.
      const cleanBody = _sanitizeHTML(body || "");
      // If the editor only contains layout chrome (e.g. an empty
      // <p> or a stray <br>), treat it as empty so the row skips
      // rendering an empty body block downstream.
      const hasContent = !!_stripHTML(cleanBody).trim();
      const created = await api.bugs.create({
        title: title.trim(),
        body: hasContent ? cleanBody : null,
        severity,
        url: typeof location !== "undefined" ? location.href : null,
        user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
      });
      // Upload attachments sequentially. Failures don't roll back the
      // bug — better to have the report without a screenshot than to
      // lose the report.
      for (const f of files) {
        try { await api.bugs.uploadAttachment(created.id, f.file); }
        catch (e) { console.warn("[bug] attachment failed:", e && e.message); }
      }
      onCreated && onCreated(created.id);
      // Reset and close.
      setTitle(""); setBody(""); setSeverity("normal"); setFiles([]);
      onClose && onClose();
    } catch (e) {
      setErr(e.message || "Couldn't submit bug.");
    } finally { setBusy(false); }
  }

  // While recording is in flight we hide the modal completely and
  // surface only a small floating "Stop" pill, so the user can drive
  // the app and reproduce the bug without the modal blocking the
  // page (which is what they're trying to capture in the first
  // place). The modal is NOT unmounted — its state (title, body,
  // severity, attachments) stays alive so when the user clicks Stop
  // the form pops back up exactly as it was, with the new clip
  // already attached.
  if (recording) {
    return (
      <div className="bug-rec-overlay">
        <div className="bug-rec-pill" role="status" aria-live="polite">
          <span className="bug-rec-dot"/>
          <span className="bug-rec-label">
            Recording this tab · <b>{recDur}s</b>
          </span>
          <button
            type="button"
            className="bug-rec-stop"
            onClick={stopRecording}
            title="Stop recording and return to the bug form">
            Stop & resume report
          </button>
        </div>
        <style>{BUG_REC_PILL_CSS}</style>
      </div>
    );
  }

  return (
    <div
      className="modal-backdrop"
      onClick={(e) => {
        // Only close when the click lands directly on the backdrop —
        // bubbled clicks from the share-picker dismiss / inner buttons
        // would otherwise tear the modal down. Switched from
        // mousedown→click because mousedown fires while the user is
        // mid-drag inside the editor (text selection) and would
        // sometimes close the modal while they were highlighting.
        if (e.target === e.currentTarget) safeClose();
      }}>
      <div className="modal" style={{ width: 620 }} onClick={e => e.stopPropagation()} onMouseDown={e => e.stopPropagation()}>
        <div className="modal-header">
          <div>
            <div className="modal-title">🐞 Report a bug</div>
            <div className="modal-subtitle">Visible to everyone in this workspace · admins triage.</div>
          </div>
          <button className="modal-close" onClick={safeClose}>✕</button>
        </div>
        <div className="modal-body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <label className="invite-form-row">
            <span>Title</span>
            <input type="text" autoFocus value={title}
                   onChange={e => setTitle(e.target.value)}
                   placeholder="One-line summary of what's broken"/>
          </label>
          <div className="invite-form-row" style={{ alignItems: "flex-start" }}>
            <span style={{ paddingTop: 8 }}>What happened?</span>
            <BugRichTextEditor
              value={body}
              onChange={setBody}
              placeholder="Steps to reproduce, what you expected, what you saw"/>
          </div>
          <label className="invite-form-row">
            <span>Severity</span>
            <select value={severity} onChange={e => setSeverity(e.target.value)}>
              {BUG_SEVERITIES.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
            </select>
          </label>

          {/* Capture row — both buttons bias the share-picker toward
              this tab (ZeroProject) since the bug report is about
              this app. The user can still override and pick a
              different surface in the picker. */}
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
            <button type="button" className="btn" onClick={takeScreenshot} disabled={busy}
                    title="Capture a screenshot of this tab. The browser will ask you to confirm the share.">
              📸 Screenshot this tab
            </button>
            {/* No need for the in-form Stop button — once recording
                starts, the whole modal is replaced by a floating
                pill (see early-return), so the form is never shown
                with `recording === true`. */}
            <button type="button" className="btn" onClick={startRecording} disabled={busy}
                    title="Record this tab. The browser will ask you to confirm the share — pick this tab and click Share. The form will hide so you can use the app; click Stop to return.">
              🎬 Record this tab
            </button>
            <label className="btn" style={{ cursor: "pointer" }}>
              📎 Attach file
              <input type="file" multiple style={{ display: "none" }}
                     accept="image/*,video/*,.log,.txt,.json,.zip"
                     onChange={e => { Array.from(e.target.files || []).forEach(addFile); e.target.value = ""; }}/>
            </label>
          </div>
          {/* In-form recording hint removed — when a recording is in
              flight the whole modal is replaced by a small floating
              "Stop" pill so the user can actually interact with the
              page they're capturing. See the early-return above. */}

          {files.length > 0 && (
            <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
              {files.map((p, i) => (
                <span key={i} style={{
                  display: "inline-flex", alignItems: "center", gap: 6,
                  padding: "4px 10px", border: "1px solid var(--border)",
                  borderRadius: 999, background: "var(--bg-subtle)", fontSize: 12,
                }}>
                  {p.kind === "image" ? "🖼" : p.kind === "video" ? "🎬" : "📎"} {p.file.name}
                  <button type="button" onClick={() => removeFile(i)}
                          style={{ border: "none", background: "transparent", cursor: "pointer",
                                   color: "var(--ink-muted)", padding: 0, fontSize: 14, lineHeight: 1 }}>×</button>
                </span>
              ))}
            </div>
          )}

          {err && <div style={{ fontSize: 12.5, color: "#c0223a", background: "#fff0f3",
                                padding: "6px 10px", borderRadius: 6 }}>{err}</div>}
          <div style={{ fontSize: 11.5, color: "var(--ink-muted)" }}>
            We'll capture the page URL ({typeof location !== "undefined" ? location.pathname : "—"}) and
            your user agent automatically so the team can reproduce.
          </div>
        </div>
        <div className="modal-footer">
          <button className="btn-ghost" onClick={safeClose} disabled={busy}>Cancel</button>
          <button className="btn-primary" onClick={submit} disabled={busy || !title.trim()}>
            {busy ? "Submitting…" : "Submit bug"}
          </button>
        </div>
      </div>
      <style>{BUG_MODAL_CSS}</style>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────
// BugRichTextEditor — small contenteditable-based rich text input.
// Toolbar: Bold, Italic, Strike, Bullet list, Numbered list, Code,
// Link, Clear formatting. Saves HTML to the value prop.
//
// Notes:
//   - We use document.execCommand. It's "deprecated" but is the only
//     path that works without a heavyweight editor library; every
//     modern browser still implements it for contenteditable.
//   - Paste is force-coerced to plain text so weird styled content
//     from Word/Slack doesn't bleed in.
//   - Initial value is set imperatively (not via React props) so the
//     cursor doesn't jump on every keystroke.
// ─────────────────────────────────────────────────────────────────────
function BugRichTextEditor({ value, onChange, placeholder }) {
  const ref = React.useRef(null);
  const lastValueRef = React.useRef(value || "");

  // Seed contents on first mount only — re-seeding on every value
  // change would yank the caret to the start mid-typing.
  React.useEffect(() => {
    if (ref.current && value && !ref.current.innerHTML) {
      ref.current.innerHTML = value;
      lastValueRef.current = value;
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Sync external value updates (e.g. parent reset on submit).
  React.useEffect(() => {
    if (!ref.current) return;
    if ((value || "") !== lastValueRef.current) {
      ref.current.innerHTML = value || "";
      lastValueRef.current = value || "";
    }
  }, [value]);

  function handleInput() {
    const html = ref.current ? ref.current.innerHTML : "";
    lastValueRef.current = html;
    onChange && onChange(html);
  }
  function exec(cmd, arg) {
    if (ref.current) ref.current.focus();
    try { document.execCommand(cmd, false, arg); } catch {}
    handleInput();
  }
  function makeLink() {
    const url = window.prompt("Link URL", "https://");
    if (!url) return;
    // Reject javascript: and data: URLs at the entry point so the
    // sanitizer doesn't have to fight them later.
    if (/^\s*(javascript|data):/i.test(url)) return;
    exec("createLink", url);
  }
  function onPaste(e) {
    e.preventDefault();
    const cd = e.clipboardData || window.clipboardData;
    const text = (cd && cd.getData("text/plain")) || "";
    try { document.execCommand("insertText", false, text); } catch {}
  }
  // Catch the common shortcuts so users with no toolbar muscle-
  // memory still get bold / italic / undo / redo.
  function onKeyDown(e) {
    const mod = e.metaKey || e.ctrlKey;
    if (!mod) return;
    const k = e.key.toLowerCase();
    if (k === "b") { e.preventDefault(); exec("bold");   }
    else if (k === "i") { e.preventDefault(); exec("italic"); }
  }

  const empty = !lastValueRef.current || !_stripHTML(lastValueRef.current).trim();

  return (
    <div className="bug-rt">
      <div className="bug-rt-toolbar" onMouseDown={e => e.preventDefault()}>
        <button type="button" onClick={() => exec("bold")} title="Bold (⌘B)"><b>B</b></button>
        <button type="button" onClick={() => exec("italic")} title="Italic (⌘I)"><i>I</i></button>
        <button type="button" onClick={() => exec("strikeThrough")} title="Strikethrough"><s>S</s></button>
        <span className="bug-rt-sep"/>
        <button type="button" onClick={() => exec("insertUnorderedList")} title="Bullet list">• List</button>
        <button type="button" onClick={() => exec("insertOrderedList")} title="Numbered list">1. List</button>
        <span className="bug-rt-sep"/>
        <button type="button" onClick={() => exec("formatBlock", "<pre>")} title="Code block">{"</>"}</button>
        <button type="button" onClick={makeLink} title="Insert link">🔗</button>
        <span className="bug-rt-sep"/>
        <button type="button" onClick={() => exec("removeFormat")} title="Clear formatting" style={{ marginLeft: "auto" }}>↺</button>
      </div>
      <div
        ref={ref}
        className={"bug-rt-area" + (empty ? " is-empty" : "")}
        contentEditable
        suppressContentEditableWarning
        onInput={handleInput}
        onPaste={onPaste}
        onKeyDown={onKeyDown}
        data-placeholder={placeholder || ""}/>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────────────
// _captureConstraints — getDisplayMedia options biased toward "this
// tab". The bug report is filed FROM the page being reported, so by
// far the most common intent is "record / screenshot this very tab".
//
//   • preferCurrentTab: true       — Chrome/Edge: picker auto-selects
//                                    the current tab. User can still
//                                    pick a different surface.
//   • selfBrowserSurface: include  — explicitly allow the calling tab
//                                    (browsers default to "exclude"
//                                    for security; we trust ourselves).
//   • surfaceSwitching: exclude    — once recording starts, the user
//                                    can't mid-stream switch to a
//                                    different surface — keeps the
//                                    capture aligned with the report.
//   • monitorTypeSurfaces: exclude — hide the "Entire screen" option
//                                    so we don't accidentally pick up
//                                    the user's other apps. Bug
//                                    reports almost never need that
//                                    much surface.
//   • displaySurface: "browser"    — hint to default the dropdown to
//                                    Tab when the picker IS shown.
//
// Browsers that don't recognise the newer fields will throw TypeError
// — both call sites catch that and retry with the plain-object form.
// ─────────────────────────────────────────────────────────────────────
function _captureConstraints({ frameRate }) {
  const video = { displaySurface: "browser" };
  if (frameRate) video.frameRate = frameRate;
  return {
    video,
    audio: false,
    preferCurrentTab: true,
    selfBrowserSurface: "include",
    surfaceSwitching: "exclude",
    monitorTypeSurfaces: "exclude",
    systemAudio: "exclude",
  };
}

// Tiny HTML helpers — kept inline so admins skim the file and see how
// the body shape is treated end-to-end. Both are hot-path safe; the
// regex sanitizer is generous (allows most tags) but blacklists the
// XSS vectors we actually care about for an internal tool.
function _sanitizeHTML(html) {
  if (!html) return "";
  return String(html)
    // Drop <script>, <iframe>, <object>, <embed>, <link>, <meta>,
    // <style> bodies wholesale.
    .replace(/<\s*(script|iframe|object|embed|link|meta|style)[\s\S]*?(<\s*\/\s*\1\s*>|$)/gi, "")
    // Strip on* event handlers (onclick, onerror, …) regardless of
    // quoting style (or no quotes).
    .replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "")
    .replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "")
    .replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "")
    // Neutralise javascript: / data: URLs in href / src.
    .replace(/(href|src)\s*=\s*"(?:\s*)(?:javascript|data):[^"]*"/gi, '$1="#"')
    .replace(/(href|src)\s*=\s*'(?:\s*)(?:javascript|data):[^']*'/gi, "$1='#'");
}
function _stripHTML(html) {
  if (!html) return "";
  return String(html).replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").trim();
}

// ─────────────────────────────────────────────────────────────────────
// BugBody — render the bug description either as HTML (when the body
// looks like markup, e.g. produced by the rich-text editor) or as
// pre-wrapped plain text (legacy rows from the old textarea). This
// keeps the rich-text upgrade backwards compatible — we don't have
// to migrate existing bug rows.
// ─────────────────────────────────────────────────────────────────────
function BugBody({ body, className, style }) {
  if (!body) return null;
  const isHTML = /<\w+[^>]*>/.test(body);
  if (isHTML) {
    return <div className={className} style={style} dangerouslySetInnerHTML={{ __html: _sanitizeHTML(body) }}/>;
  }
  return <div className={className} style={{ whiteSpace: "pre-wrap", ...(style || {}) }}>{body}</div>;
}

const BUG_MODAL_CSS = `
.bug-rt {
  display: flex; flex-direction: column;
  flex: 1; min-width: 0;
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 8px;
  background: #fff;
  overflow: hidden;
  font-size: 13px;
}
.bug-rt-toolbar {
  display: flex; align-items: center; gap: 2px;
  padding: 4px 6px;
  background: var(--surface-2, #f6f7fb);
  border-bottom: 1px solid var(--border, #e6e9ef);
  flex-wrap: wrap;
}
.bug-rt-toolbar button {
  border: none; background: transparent;
  padding: 4px 8px; min-width: 28px; height: 26px;
  font: inherit; font-size: 12px;
  color: var(--ink, #0f1729); cursor: pointer;
  border-radius: 4px;
  display: inline-flex; align-items: center; justify-content: center;
}
.bug-rt-toolbar button:hover { background: rgba(15,23,41,.06); }
.bug-rt-sep {
  width: 1px; height: 18px;
  background: var(--border, #e6e9ef);
  margin: 0 4px;
}
.bug-rt-area {
  min-height: 120px; max-height: 300px; overflow: auto;
  padding: 10px 12px;
  outline: none;
  font: inherit; font-size: 13.5px; line-height: 1.5;
  color: var(--ink, #0f1729);
}
.bug-rt-area.is-empty::before {
  content: attr(data-placeholder);
  color: var(--ink-muted, #9aa0a6);
  pointer-events: none;
  display: block;
}
.bug-rt-area p { margin: 0 0 8px 0; }
.bug-rt-area p:last-child { margin-bottom: 0; }
.bug-rt-area ul, .bug-rt-area ol { margin: 6px 0; padding-left: 22px; }
.bug-rt-area pre {
  background: var(--surface-2, #f1f4f9);
  padding: 8px 10px; border-radius: 6px;
  font-size: 12.5px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  overflow-x: auto;
  margin: 6px 0;
}
.bug-rt-area a { color: var(--brand, #0073ea); }
.bug-rt-area:focus { box-shadow: inset 0 0 0 2px rgba(0,115,234,.18); }
`;

// While a recording is in flight we drop the modal entirely and
// surface only this floating pill, so the user can drive the page
// they're trying to capture. Pinned bottom-center because top-right
// is already busy with the chat bubble + bug-floating-button stack
// and we don't want to block any of them.
const BUG_REC_PILL_CSS = `
.bug-rec-overlay {
  position: fixed; left: 0; right: 0; bottom: 18px;
  display: flex; justify-content: center; pointer-events: none;
  z-index: 2147483646;
}
.bug-rec-pill {
  pointer-events: auto;
  display: inline-flex; align-items: center; gap: 12px;
  padding: 8px 8px 8px 14px;
  background: rgba(15,23,41,.94);
  color: #fff;
  border-radius: 999px;
  box-shadow: 0 12px 32px rgba(15,23,41,.32), 0 2px 6px rgba(15,23,41,.18);
  font-size: 13px; font-weight: 600;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.bug-rec-dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: #ff4757;
  box-shadow: 0 0 0 0 rgba(255,71,87,.7);
  animation: bug-rec-pulse 1.1s ease-in-out infinite;
}
@keyframes bug-rec-pulse {
  0%   { box-shadow: 0 0 0 0   rgba(255,71,87,.6); }
  60%  { box-shadow: 0 0 0 9px rgba(255,71,87,0);  }
  100% { box-shadow: 0 0 0 0   rgba(255,71,87,0);  }
}
.bug-rec-label { color: rgba(255,255,255,.94); letter-spacing: .01em; }
.bug-rec-label b { color: #fff; }
.bug-rec-stop {
  background: #ff4757; color: #fff;
  border: none; cursor: pointer;
  padding: 7px 16px; border-radius: 999px;
  font: inherit; font-size: 12.5px; font-weight: 700;
  box-shadow: 0 2px 6px rgba(255,71,87,.45);
  transition: transform .12s ease, background .12s ease;
}
.bug-rec-stop:hover { background: #ff5e6c; transform: translateY(-1px); }
.bug-rec-stop:active { transform: translateY(0); }
`;

// ── Bugs page (sidebar entry: workspace-shared list) ────────────────
function BugsView({ currentUserId, me: propMe }) {
  const [bugs, setBugs]       = React.useState(null);
  const [filter, setFilter]   = React.useState("active");   // active | all | open | investigating | fixed | wont_fix
  const [openId, setOpenId]   = React.useState(null);
  const [statusMenuId, setStatusMenuId] = React.useState(null);  // which row's status menu is open
  const [busyStatusId, setBusyStatusId] = React.useState(null);  // row currently saving
  const [me, setMe] = React.useState(() => (window.api && api.getUser && api.getUser()) || null);
  // Role resolution is tolerant: the app passes the SHAPED current user
  // (with wsRole) as `me`, but api.getUser() returns the raw login user
  // which historically had no role field at all — that made isAdmin
  // always false, hiding the status-change control for everyone (incl.
  // owners). Check the prop first, then both field casings on the
  // stored user.
  const _role = (propMe && propMe.wsRole)
    || (me && (me.wsRole || me.ws_role))
    || null;
  const isAdmin = _role === "owner" || _role === "admin";

  async function load() {
    try {
      const params = filter === "active" ? {}
                   : filter === "all"    ? {}
                   : { status: filter };
      let list = await api.bugs.list(params);
      if (filter === "active") {
        list = list.filter(b => b.status === "open" || b.status === "investigating");
      }
      setBugs(list);
    } catch (e) { setBugs([]); }
  }
  React.useEffect(() => { load(); /* eslint-disable-next-line */ }, [filter]);

  // Inline status change directly from the list — admins only.
  async function changeRowStatus(bugId, nextStatus) {
    setBusyStatusId(bugId);
    setStatusMenuId(null);
    // Optimistic UI: flip the row immediately, roll back on failure.
    setBugs(prev => prev
      ? prev.map(b => b.id === bugId ? { ...b, status: nextStatus } : b)
      : prev);
    try {
      await api.bugs.setStatus(bugId, { status: nextStatus });
      await load();
    } catch (e) {
      // Roll back the optimistic change
      await load();
      const msg = (e && e.body && e.body.detail) ||
                  (e && e.message) || "Couldn't change status";
      if (typeof window.fbToast === "function") window.fbToast(msg, 4000);
    } finally {
      setBusyStatusId(null);
    }
  }

  // Close the status menu when clicking anywhere else
  React.useEffect(() => {
    if (!statusMenuId) return;
    function onDown(e) {
      const el = document.querySelector(".bug-status-menu");
      if (el && !el.contains(e.target) && !e.target.closest(".bug-status-pill")) {
        setStatusMenuId(null);
      }
    }
    function onKey(e) { if (e.key === "Escape") setStatusMenuId(null); }
    document.addEventListener("mousedown", onDown);
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("mousedown", onDown);
      document.removeEventListener("keydown", onKey);
    };
  }, [statusMenuId]);

  return (
    <div className="sup-root">
      <div className="sup-header">
        <div className="sup-title-row">
          <h1 className="sup-title"><span className="sup-title-icon">🐞</span> Bug reports</h1>
          <div className="sup-meta">Anyone in the workspace can see and report. Admins triage and mark fixed.</div>
        </div>
        <div className="sup-tabs">
          {[
            { id: "active",        label: "Active" },
            { id: "open",          label: "Open" },
            { id: "investigating", label: "Investigating" },
            { id: "fixed",         label: "Fixed" },
            { id: "wont_fix",      label: "Won't fix" },
            { id: "all",           label: "All" },
          ].map(t => (
            <button key={t.id} className={`sup-tab ${filter === t.id ? "is-active" : ""}`}
                    onClick={() => setFilter(t.id)}>{t.label}</button>
          ))}
        </div>
      </div>
      <div className="sup-tickets">
        <div className="sup-tickets-toolbar">
          <div className="sup-toolbar-spacer">
            {bugs && <span className="sup-toolbar-count">{bugs.length} bug{bugs.length === 1 ? "" : "s"}</span>}
            <button className="btn" onClick={load}>Refresh</button>
          </div>
        </div>
        <div className="sup-ticket-grid is-full">
          <div className="sup-ticket-col">
            {bugs === null ? (
              <div className="sup-empty">Loading…</div>
            ) : bugs.length === 0 ? (
              <div className="sup-empty">Nothing here. {filter === "active" && "All bugs are fixed or closed — nice."}</div>
            ) : (
              <div className="sup-group" style={{ "--sup-group-color": "#e2445c" }}>
                <div className="sup-group-head">
                  <svg className="sup-group-chev" viewBox="0 0 16 16" fill="none">
                    <path d="M5 6l3 3 3-3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                  <span className="sup-group-title">{filter === "active" ? "Active bugs" : filter.replace(/_/g, " ")}</span>
                  <span className="sup-group-count">{bugs.length}</span>
                </div>
                <div className="sup-group-table-wrap">
                  <table className="sup-t">
                    <thead><tr>
                      <th className="sup-th-name">Title</th>
                      <th>Reporter</th>
                      <th className="sup-th-center">Severity</th>
                      <th className="sup-th-center">Status</th>
                      <th className="sup-th-center">Files</th>
                      <th>Reported</th>
                      <th>Fixed</th>
                    </tr></thead>
                    <tbody>
                      {bugs.map(b => {
                        const sev = _bugSev(b.severity), st = _bugStat(b.status);
                        return (
                          <tr key={b.id} className={`sup-tr ${openId === b.id ? "is-selected" : ""}`}
                              onClick={() => setOpenId(b.id)}>
                            <td className="sup-td-name">
                              <div className="sup-td-name-wrap"><span>{b.title}</span></div>
                              <div className="sup-td-id">{b.id}</div>
                            </td>
                            <td>
                              <div className="sup-td-customer">{b.reporter_name || "—"}</div>
                            </td>
                            <td className="sup-td-center">
                              <span style={{
                                display: "inline-flex", padding: "2px 9px", borderRadius: 999,
                                fontSize: 10.5, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase",
                                background: sev.color + "22", color: sev.color,
                              }}>{sev.label}</span>
                            </td>
                            <td className="sup-td-center" style={{ position: "relative", overflow: "visible" }}>
                              {isAdmin ? (
                                <button
                                  className="bug-status-pill"
                                  disabled={busyStatusId === b.id}
                                  onClick={(e) => {
                                    e.stopPropagation();
                                    setStatusMenuId(prev => prev === b.id ? null : b.id);
                                  }}
                                  title="Click to change status"
                                  style={{
                                    display: "inline-flex", alignItems: "center", gap: 4,
                                    padding: "2px 8px 2px 9px", borderRadius: 999,
                                    fontSize: 10.5, fontWeight: 800, letterSpacing: ".04em",
                                    textTransform: "uppercase",
                                    background: st.color + "22", color: st.color,
                                    border: "0", cursor: busyStatusId === b.id ? "wait" : "pointer",
                                    opacity: busyStatusId === b.id ? 0.6 : 1,
                                    fontFamily: "inherit",
                                  }}
                                >
                                  {st.label}
                                  <span style={{ fontSize: 8, opacity: .7, marginTop: 1 }}>▾</span>
                                </button>
                              ) : (
                                <span style={{
                                  display: "inline-flex", padding: "2px 9px", borderRadius: 999,
                                  fontSize: 10.5, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase",
                                  background: st.color + "22", color: st.color,
                                }}>{st.label}</span>
                              )}
                              {statusMenuId === b.id && isAdmin && (
                                <div
                                  className="bug-status-menu"
                                  onClick={(e) => e.stopPropagation()}
                                  onMouseDown={(e) => e.stopPropagation()}
                                  style={{
                                    position: "absolute",
                                    top: "calc(100% + 4px)",
                                    left: "50%",
                                    transform: "translateX(-50%)",
                                    background: "#fff",
                                    border: "1px solid #e6e9ef",
                                    borderRadius: 10,
                                    boxShadow: "0 8px 24px rgba(15,23,42,.12), 0 2px 6px rgba(15,23,42,.06)",
                                    padding: 4,
                                    zIndex: 10100,
                                    minWidth: 160,
                                    display: "flex",
                                    flexDirection: "column",
                                    gap: 2,
                                  }}
                                >
                                  {BUG_STATUSES.map(opt => (
                                    <button
                                      key={opt.id}
                                      onClick={(e) => {
                                        e.stopPropagation();
                                        if (opt.id !== b.status) changeRowStatus(b.id, opt.id);
                                        else setStatusMenuId(null);
                                      }}
                                      style={{
                                        display: "flex", alignItems: "center", gap: 8,
                                        padding: "6px 10px", border: 0, borderRadius: 6,
                                        background: opt.id === b.status ? "#f3f4f7" : "transparent",
                                        cursor: "pointer",
                                        textAlign: "left",
                                        fontFamily: "inherit",
                                        fontSize: 12,
                                        color: "#0f1729",
                                        fontWeight: 500,
                                      }}
                                      onMouseEnter={e => { if (opt.id !== b.status) e.currentTarget.style.background = "#fafbfd"; }}
                                      onMouseLeave={e => { if (opt.id !== b.status) e.currentTarget.style.background = "transparent"; }}
                                    >
                                      <span style={{
                                        display: "inline-block", width: 8, height: 8, borderRadius: 999,
                                        background: opt.color, flexShrink: 0,
                                      }}/>
                                      <span style={{ flex: 1 }}>{opt.label}</span>
                                      {opt.id === b.status && (
                                        <span style={{ color: "#00853d", fontSize: 11, fontWeight: 700 }}>✓</span>
                                      )}
                                    </button>
                                  ))}
                                </div>
                              )}
                            </td>
                            <td className="sup-td-center">
                              {b.attachment_count
                                ? <span className="sup-credits-pill">📎 {b.attachment_count}</span>
                                : <span className="sup-faint">—</span>}
                            </td>
                            <td className="sup-td-meta">{_bugRel(b.created_at)}</td>
                            <td className="sup-td-meta">
                              {b.fixed_at
                                ? <span style={{ color: "#00853d" }}>✓ {_bugRel(b.fixed_at)}{b.fixer_name ? ` · ${b.fixer_name.split(" ")[0]}` : ""}</span>
                                : <span className="sup-faint">—</span>}
                            </td>
                          </tr>
                        );
                      })}
                    </tbody>
                  </table>
                </div>
              </div>
            )}
          </div>
        </div>
      </div>
      {openId && (
        <BugDrawer id={openId} isAdmin={isAdmin} currentUserId={currentUserId}
                   onChanged={load} onClose={() => setOpenId(null)}/>
      )}
    </div>
  );
}

// ── Bug detail drawer ──────────────────────────────────────────────
function BugDrawer({ id, isAdmin, currentUserId, onChanged, onClose }) {
  const [b, setB]           = React.useState(null);
  const [reply, setReply]   = React.useState("");
  const [fixOpen, setFixOpen] = React.useState(false);
  const [fixNote, setFixNote] = React.useState("");
  const [busy, setBusy]     = React.useState(false);

  async function load() {
    try { setB(await api.bugs.get(id)); } catch { setB(null); }
  }
  React.useEffect(() => { load(); /* eslint-disable-next-line */ }, [id]);
  React.useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose && onClose(); }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  async function setStatus(next, note = null) {
    setBusy(true);
    try {
      await api.bugs.setStatus(id, { status: next, fix_note: note });
      await load();
      onChanged && onChanged();
    } finally { setBusy(false); }
  }
  async function markFixed() {
    if (!fixNote.trim()) { alert("Add a brief fix note so the reporter knows what changed."); return; }
    await setStatus("fixed", fixNote.trim());
    setFixNote(""); setFixOpen(false);
  }
  async function send() {
    if (!reply.trim()) return;
    setBusy(true);
    try {
      await api.bugs.addComment(id, reply.trim());
      setReply("");
      await load();
    } finally { setBusy(false); }
  }

  if (!b) return (
    <div className="sup-drawer-root" onClick={onClose}>
      <div className="sup-drawer-backdrop"/>
      <div className="sup-drawer" onClick={e => e.stopPropagation()}>
        <button className="sup-drawer-close" onClick={onClose}>
          <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
            <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/>
          </svg>
        </button>
        <div style={{ padding: 40, color: "var(--ink-muted)" }}>Loading…</div>
      </div>
    </div>
  );

  const sev = _bugSev(b.severity), st = _bugStat(b.status);

  return (
    <div className="sup-drawer-root" onClick={onClose}>
      <div className="sup-drawer-backdrop"/>
      <div className="sup-drawer" onClick={e => e.stopPropagation()}>
        <button className="sup-drawer-close" onClick={onClose}>
          <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
            <path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/>
          </svg>
        </button>
        <div className="sup-detail-head" style={{ borderLeftColor: sev.color }}>
          <div>
            <div className="sup-detail-id">{b.id}</div>
            <h3>{b.title}</h3>
            <div className="sup-detail-cust">
              {b.reporter_name && <><b>{b.reporter_name}</b><span>·</span></>}
              <span>{_bugRel(b.created_at)}</span>
              {b.url && <><span>·</span><span style={{ fontFamily: "ui-monospace, monospace", fontSize: 11 }}>{b.url}</span></>}
            </div>
            <div className="sup-detail-meta">
              <span style={{
                display: "inline-flex", padding: "3px 9px", borderRadius: 999,
                fontSize: 10.5, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase",
                background: sev.color + "22", color: sev.color,
              }}>{sev.label}</span>
              <span style={{
                display: "inline-flex", padding: "3px 9px", borderRadius: 999,
                fontSize: 10.5, fontWeight: 800, letterSpacing: ".04em", textTransform: "uppercase",
                background: st.color + "22", color: st.color,
              }}>{st.label}</span>
            </div>
          </div>
        </div>

        {isAdmin && (
          <div className="sup-detail-controls">
            <label>Status
              <select value={b.status} onChange={e => setStatus(e.target.value)} disabled={busy}>
                {BUG_STATUSES.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
              </select>
            </label>
            <span style={{ marginLeft: "auto" }}>
              {b.status !== "fixed" && (
                <button className="btn btn-primary" onClick={() => setFixOpen(true)} disabled={busy}>
                  ✓ Mark fixed
                </button>
              )}
            </span>
          </div>
        )}

        {fixOpen && (
          <div style={{ padding: "10px 16px", borderBottom: "1px solid var(--border-row)",
                        background: "linear-gradient(0deg, rgba(0,200,117,.06), rgba(0,200,117,.06)) white" }}>
            <div style={{ fontSize: 12, fontWeight: 700, color: "var(--ink-strong)", marginBottom: 6 }}>
              Mark fixed — fix log entry (visible to the reporter)
            </div>
            <textarea rows={3} value={fixNote} onChange={e => setFixNote(e.target.value)}
                      placeholder="What did you change? e.g. 'Status pill colors were swapped — flipped the lookup keys in support.css.'"
                      style={{ width: "100%", padding: "8px 10px", border: "1px solid var(--border)",
                               borderRadius: 6, fontSize: 13 }}/>
            <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 8 }}>
              <button className="btn" onClick={() => { setFixOpen(false); setFixNote(""); }} disabled={busy}>Cancel</button>
              <button className="btn btn-primary" onClick={markFixed} disabled={busy || !fixNote.trim()}>
                {busy ? "Saving…" : "Save fix"}
              </button>
            </div>
          </div>
        )}

        {/* Fix log if fixed */}
        {b.status === "fixed" && b.fix_note && (
          <div style={{
            margin: "12px 16px 0", padding: "12px 14px", borderRadius: 8,
            background: "rgba(0,200,117,.10)", border: "1px solid rgba(0,200,117,.30)",
          }}>
            <div style={{ fontSize: 11, fontWeight: 800, letterSpacing: ".06em",
                          textTransform: "uppercase", color: "#00853d", marginBottom: 6 }}>
              ✓ Fix log
            </div>
            <div style={{ fontSize: 13.5, color: "var(--ink-strong)", whiteSpace: "pre-wrap" }}>{b.fix_note}</div>
            <div style={{ fontSize: 11.5, color: "var(--ink-muted)", marginTop: 6 }}>
              {b.fixer_name || "Admin"} · {_bugRel(b.fixed_at)}
            </div>
          </div>
        )}

        {b.body && <BugBody className="sup-detail-body" body={b.body}/>}

        {b.attachments && b.attachments.length > 0 && (
          <div className="sup-attachments">
            {b.attachments.map(a => (
              a.kind === "video"
                ? <video key={a.id} src={a.url} controls className="sup-att-video"/>
                : a.kind === "image"
                ? <a key={a.id} href={a.url} target="_blank" rel="noopener noreferrer">
                    <img src={a.url} alt={a.original_name}
                         style={{ maxWidth: 220, maxHeight: 160, borderRadius: 8,
                                  border: "1px solid var(--border-soft)" }}/>
                  </a>
                : <a key={a.id} href={a.url} target="_blank" rel="noopener noreferrer" className="sup-att-file">
                    📎 {a.original_name}
                  </a>
            ))}
          </div>
        )}

        {/* Comments / fix log thread */}
        <div className="sup-thread">
          {(b.comments || []).map(c => {
            const isMe = c.author_id === currentUserId;
            return (
              <div key={c.id} className={`sup-comment ${isMe ? "is-agent" : "is-customer"}`}
                   style={c.is_fix_note ? { background: "rgba(0,200,117,.10)", border: "1px solid rgba(0,200,117,.25)" } : undefined}>
                <div className="sup-comment-meta">
                  {c.is_fix_note && "✓ Fix · "}{c.author_name || "Someone"} · {_bugRel(c.created_at)}
                </div>
                <div className="sup-comment-body">{c.body}</div>
              </div>
            );
          })}
          {(!b.comments || !b.comments.length) && (
            <div className="sup-thread-empty">No comments yet.</div>
          )}
        </div>

        <div className="sup-reply">
          <textarea rows={3} placeholder="Add a comment / note…" value={reply}
                    onChange={e => setReply(e.target.value)}/>
          <div className="sup-reply-actions">
            <button className="btn btn-primary" disabled={busy || !reply.trim()} onClick={send}>
              {busy ? "Sending…" : "Send"}
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { BugFloatingButton, BugReportModal, BugsView, BugRichTextEditor, BugBody });
