| 1 |
// ===== write-with-ai/index.js — mounts the redesigned "Write with AI" modal ===== |
| 2 |
// Replaces the legacy inline PHP/jQuery template. Registers a hidden editor |
| 3 |
// plugin that (a) injects the "Write with AI" button into the editor header and |
| 4 |
// (b) renders the StudioGenerateModal via a portal. |
| 5 |
|
| 6 |
import { __ } from "@wordpress/i18n"; |
| 7 |
import { registerPlugin } from "@wordpress/plugins"; |
| 8 |
import { useState, useEffect, useRef, createPortal } from "@wordpress/element"; |
| 9 |
import { subscribe } from "@wordpress/data"; |
| 10 |
|
| 11 |
import { AIMarkWhite } from "../ai-shared/kit"; |
| 12 |
import StudioGenerateModal from "./StudioGenerateModal"; |
| 13 |
|
| 14 |
// Header trigger button — styled with the shared kit tokens (wrapped in |
| 15 |
// `.bd-ai-scope` because it is portaled into the WP editor header, outside the |
| 16 |
// modal scope). |
| 17 |
const HeaderButton = ({ onClick }) => ( |
| 18 |
<span className="bd-ai-scope" style={{ display: "inline-flex", marginRight: 8 }}> |
| 19 |
<button type="button" onClick={onClick} className="bd-write-with-ai-button" style={{ |
| 20 |
display: "inline-flex", alignItems: "center", gap: 8, |
| 21 |
background: "var(--green)", color: "#fff", border: "none", |
| 22 |
height: 36, padding: "0 14px", borderRadius: 8, cursor: "pointer", |
| 23 |
fontSize: 13, fontWeight: 700, whiteSpace: "nowrap", |
| 24 |
boxShadow: "0 1px 0 rgba(0,0,0,.04), inset 0 -1px 0 rgba(0,0,0,.06)", |
| 25 |
transition: "background .15s", |
| 26 |
}} |
| 27 |
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--green-600)"; }} |
| 28 |
onMouseLeave={(e) => { e.currentTarget.style.background = "var(--green)"; }}> |
| 29 |
<span style={{ display: "flex" }}><AIMarkWhite size={16} /></span> |
| 30 |
{__("Write with AI", "betterdocs")} |
| 31 |
</button> |
| 32 |
</span> |
| 33 |
); |
| 34 |
|
| 35 |
const findHeaderNode = () => |
| 36 |
document.querySelector(".edit-post-header__settings") || |
| 37 |
document.querySelector(".editor-header__settings"); |
| 38 |
|
| 39 |
const WriteWithAIApp = () => { |
| 40 |
const [open, setOpen] = useState(false); |
| 41 |
const [host, setHost] = useState(null); |
| 42 |
const hostRef = useRef(null); |
| 43 |
const headerRef = useRef(null); |
| 44 |
|
| 45 |
// Watch the editor chrome until the header settings area exists, then mount |
| 46 |
// the button there. We render into our own host node and PREPEND it (first |
| 47 |
// child) so it sits beside the Git Sync button — matching master, where the |
| 48 |
// legacy jQuery did `$('.…-header__settings').prepend(bdAIButton)`. A plain |
| 49 |
// portal would append (far right) instead. |
| 50 |
useEffect(() => { |
| 51 |
let mounted = true; |
| 52 |
if (!hostRef.current) { |
| 53 |
const node = document.createElement("span"); |
| 54 |
// `betterdocs-ai-button-container` is the anchor Pro's Git Sync looks |
| 55 |
// for: it inserts its button at `aiContainer.nextSibling`, so this |
| 56 |
// node ends up directly to the LEFT of the Git Sync button (master |
| 57 |
// parity — the legacy free button used the same wrapper class). |
| 58 |
node.className = "betterdocs-ai-button-container bd-write-with-ai-host"; |
| 59 |
hostRef.current = node; |
| 60 |
} |
| 61 |
const sync = () => { |
| 62 |
const header = findHeaderNode(); |
| 63 |
if (!header || header === headerRef.current) return; |
| 64 |
headerRef.current = header; |
| 65 |
header.prepend(hostRef.current); |
| 66 |
if (mounted && !host) setHost(hostRef.current); |
| 67 |
}; |
| 68 |
sync(); |
| 69 |
const unsubscribe = subscribe(() => { window.requestAnimationFrame(sync); }); |
| 70 |
return () => { mounted = false; unsubscribe(); }; |
| 71 |
}, [host]); |
| 72 |
|
| 73 |
return ( |
| 74 |
<> |
| 75 |
{host && createPortal(<HeaderButton onClick={() => setOpen(true)} />, host)} |
| 76 |
<StudioGenerateModal open={open} onClose={() => setOpen(false)} /> |
| 77 |
</> |
| 78 |
); |
| 79 |
}; |
| 80 |
|
| 81 |
// Registered unconditionally — the render slot (PluginArea) only mounts inside |
| 82 |
// the block editor, so this is inert on the classic editor. The bundle itself is |
| 83 |
// enqueued by PHP only on the `docs` editor screen. |
| 84 |
registerPlugin("betterdocs-write-with-ai", { render: WriteWithAIApp }); |
| 85 |
|