import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.tsx' import './index.css' import { TARGET_ENV, DEPLOYMENT_ENV, NONCE_REFRESH_URL, NONCE_REFRESH_NONCE, API_BASE_URL, ASSISTANT_ENABLED, getOrCreatePluginSessionId, getAuthToken, getRestNonce, setRestNonce } from './constants.ts' // Automatically attach WP REST nonce for same-origin requests when running inside WordPress. // If the nonce expires during a long-lived admin session, refresh it once and retry. if (TARGET_ENV === 'wordpress') { const originalFetch = window.fetch; let nonceRefreshPromise: Promise | null = null; const normalizeUrl = (value: string): string => { try { return new URL(value, window.location.origin).toString(); } catch { return value; } }; const isNonceFailure = async (response: Response): Promise => { if (response.status !== 403) return false; try { const payload = await response.clone().json(); const code = typeof payload?.code === 'string' ? payload.code : ''; const message = typeof payload?.message === 'string' ? payload.message : ''; return code === 'rest_cookie_invalid_nonce' || /nonce/i.test(message); } catch { return false; } }; const normalizedAssistantApiBaseUrl = normalizeUrl(API_BASE_URL).replace(/\/+$/, ''); const isAssistantApiRequest = (requestUrl: URL | null): boolean => { if (!requestUrl || !normalizedAssistantApiBaseUrl) return false; const normalizedRequestUrl = normalizeUrl(requestUrl.toString()); return normalizedRequestUrl === normalizedAssistantApiBaseUrl || normalizedRequestUrl.startsWith(`${normalizedAssistantApiBaseUrl}/`); }; const refreshRestNonce = async (): Promise => { if (!NONCE_REFRESH_URL) return null; if (nonceRefreshPromise) return nonceRefreshPromise; nonceRefreshPromise = (async () => { const response = await originalFetch(NONCE_REFRESH_URL, { method: 'POST', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', }, body: new URLSearchParams({ nonce: NONCE_REFRESH_NONCE, }), }); if (!response.ok) { return null; } const payload = await response.json().catch(() => null); const freshNonce = typeof payload?.data?.restNonce === 'string' ? payload.data.restNonce : typeof payload?.restNonce === 'string' ? payload.restNonce : null; if (freshNonce) { setRestNonce(freshNonce); } return freshNonce; })().finally(() => { nonceRefreshPromise = null; }); return nonceRefreshPromise; }; window.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { const reqInit: RequestInit = init ? { ...init } : {}; const inputHeaders = input instanceof Request ? input.headers : undefined; const baseHeaders = new Headers(inputHeaders ?? undefined); if (reqInit.headers) { new Headers(reqInit.headers).forEach((value, key) => { baseHeaders.set(key, value); }); } let requestUrl: URL | null = null; let isSameOrigin = false; let isNonceRefreshRequest = false; let isAssistantRequest = false; try { if (typeof input === 'string' || input instanceof URL) { requestUrl = new URL(input.toString(), window.location.origin); } else { requestUrl = new URL(input.url, window.location.origin); } isSameOrigin = requestUrl.origin === window.location.origin; isNonceRefreshRequest = normalizeUrl(requestUrl.toString()) === normalizeUrl(NONCE_REFRESH_URL); isAssistantRequest = isAssistantApiRequest(requestUrl); } catch { // Ignore malformed URL. } if (requestUrl && !isSameOrigin) { reqInit.credentials = 'omit'; } try { if (ASSISTANT_ENABLED && isAssistantRequest) { const pluginSession = getOrCreatePluginSessionId(); baseHeaders.set('X-Plugin-Session', pluginSession); const token = getAuthToken(); if (token) baseHeaders.set('Authorization', `Bearer ${token}`); } } catch {} const performFetch = (nonce: string | null): Promise => { const headers = new Headers(baseHeaders); if (isSameOrigin && nonce) { headers.set('X-WP-Nonce', nonce); } else if (isSameOrigin) { headers.delete('X-WP-Nonce'); } return originalFetch(input, { ...reqInit, headers }); }; const initialNonce = getRestNonce(); let response = await performFetch(initialNonce || null); if (!isSameOrigin || isNonceRefreshRequest || !(await isNonceFailure(response))) { return response; } const freshNonce = await refreshRestNonce(); if (!freshNonce || freshNonce === initialNonce) { return response; } response = await performFetch(freshNonce); return response; }; } function mountInShadowDom() { const el = document.querySelector('actionpanel-ai-app') as HTMLElement | null; if (!el) return false; const shadowRoot = el.attachShadow({ mode: 'open' }); // --- Fit #actionpanel-ai-root exactly to viewport bottom (throttled) --- const rootContainer = document.getElementById('actionpanel-ai-root')!; const getViewportHeight = () => (window as any).visualViewport?.height ?? window.innerHeight; const fitNow = () => { const rect = rootContainer.getBoundingClientRect(); const avail = Math.max(0, getViewportHeight() - rect.top); // Set both logical & physical for broad engine support rootContainer.style.blockSize = `${avail}px`; rootContainer.style.height = `${avail}px`; // Expose admin bar height to the shadow tree const adminBar = document.getElementById('wpadminbar'); const barH = adminBar ? adminBar.offsetHeight : 0; // put it on the shadow HOST so components can use it el.style.setProperty('--actionpanel-ai-admin-bar-h', `${barH}px`); }; // RAF throttle to avoid layout thrash during bursts let rafId: number | null = null; const scheduleFit = () => { if (rafId != null) return; rafId = requestAnimationFrame(() => { rafId = null; fitNow(); }); }; // Initial fitNow(); // Window resize/orientation window.addEventListener('resize', scheduleFit, { passive: true }); window.addEventListener('orientationchange', scheduleFit, { passive: true }); // Visual viewport (mobile keyboard etc.) (window as any).visualViewport?.addEventListener?.('resize', scheduleFit, { passive: true }); // Observe size changes of admin chrome that affect available height const ro = new ResizeObserver(scheduleFit); const adminBar = document.getElementById('wpadminbar'); const wpBodyContent = document.getElementById('wpbody-content'); if (adminBar) ro.observe(adminBar); if (wpBodyContent) ro.observe(wpBodyContent); // If you still want to react to notices being inserted/removed, use a lean MO: const mo = new MutationObserver(() => scheduleFit()); if (wpBodyContent) mo.observe(wpBodyContent, { childList: true }); // Prepare sizing stylesheet (APPEND LAST, after index.css) const sizing = document.createElement('style'); sizing.textContent = ` :host { display:block; width:100%; height:100%; } #actionpanel-ai-mount { width:100%; height:100%; overflow:auto; box-sizing:border-box; background:#f8fafc; } #actionpanel-ai-mount.dark { background:#0f172a; } /* OVERRIDES that must beat Tailwind */ .h-screen, .min-h-screen { height:100% !important; min-height:0 !important; } .h-mobile-safe { height:100% !important; min-height:0 !important; } /* Ensure the first app wrapper fills the mount */ #actionpanel-ai-mount > :first-child { height: 100% !important; min-height: 0 !important; } /* Flex helpers */ .min-h-0 { min-height:0 !important; } `; // Create mount node (same as yours) const mount = document.createElement('div'); mount.id = 'actionpanel-ai-mount'; mount.style.visibility = 'hidden'; shadowRoot.appendChild(mount); (window as any).actionPanelAiSetTheme = (mode:'light'|'dark') => mount.classList.toggle('dark', mode === 'dark'); const renderApp = () => { const node = (DEPLOYMENT_ENV === 'production' ? : ); createRoot(mount).render(node); requestAnimationFrame(() => { mount.style.visibility = ''; }); }; const cssUrl = (window as any)?.actionPanelAiConfig?.cssUrl; if (cssUrl) { fetch(cssUrl) .then(r => r.text()) .then(cssText => { const supportsConstructable = 'adoptedStyleSheets' in Document.prototype && 'replaceSync' in CSSStyleSheet.prototype; if (supportsConstructable) { const sheet = new CSSStyleSheet(); sheet.replaceSync(cssText); // @ts-ignore shadowRoot.adoptedStyleSheets = [sheet]; // ⬇️ APPEND SIZING LAST shadowRoot.appendChild(sizing); } else { const style = document.createElement('style'); style.textContent = cssText; shadowRoot.appendChild(style); // ⬇️ APPEND SIZING LAST shadowRoot.appendChild(sizing); } renderApp(); }) .catch(() => { // On failure, still ensure sizing exists shadowRoot.appendChild(sizing); renderApp(); }); } else { shadowRoot.appendChild(sizing); renderApp(); } return true; } if (TARGET_ENV === 'wordpress') { // Try Shadow DOM; if not present (legacy), fall back to #root if (!mountInShadowDom()) { const root = document.getElementById('root'); if (root) { const node = ( DEPLOYMENT_ENV === 'production' ? : ( ) ); createRoot(root).render(node); } } } else { // Web build const root = document.getElementById('root'); if (root) { const node = ( DEPLOYMENT_ENV === 'production' ? : ( ) ); createRoot(root).render(node); } }