| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* Placeholder rail for the deferred docked-left agent. |
| 5 |
*/ |
| 6 |
|
| 7 |
namespace Extendify\Agent; |
| 8 |
|
| 9 |
defined('ABSPATH') || die('No direct access.'); |
| 10 |
|
| 11 |
use Extendify\PartnerData; |
| 12 |
|
| 13 |
/** |
| 14 |
* Prints the sidebar's chrome and puts the page content where SidebarLayout's |
| 15 |
* useLayoutShift will put it, so the deferred agent mounting a beat after |
| 16 |
* render doesn't move the page under the reader. The markup mirrors |
| 17 |
* SidebarLayout and ChatInput; the agent stylesheet ships at render, so those |
| 18 |
* class strings style it. Boot drops all of it once the panel commits. |
| 19 |
*/ |
| 20 |
class Skeleton |
| 21 |
{ |
| 22 |
// phpcs:disable PSR12.Properties.ConstantVisibility.NotFound -- 7.0 floor: no const visibility |
| 23 |
// Mirrors SidebarLayout's SIDEBAR_WIDTH / FRAME_WIDTH / translateY; |
| 24 |
// drift shows up as a jump on mount. |
| 25 |
const WIDTH = 384; |
| 26 |
const FRAME = 8; |
| 27 |
const TOP = 40; |
| 28 |
|
| 29 |
// The global store seeds isMobile from this width, and MobileLayout |
| 30 |
// offsets nothing. |
| 31 |
const DESKTOP = 783; |
| 32 |
|
| 33 |
// Shorter than this and a warm cache turns the rail into a flash. |
| 34 |
const MIN_VISIBLE = 1500; |
| 35 |
|
| 36 |
// phpcs:disable Generic.Files.LineLength.TooLong -- inline SVG paths |
| 37 |
// src/Agent/icons.jsx `magic`, which AdminBar renders at 20px. |
| 38 |
const MAGIC_ICON = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">' |
| 39 |
. '<path d="M17.0909 9.81818L18 7.81818L20 6.90909L18 6L17.0909 4L16.1818 6L14.1818 6.90909L16.1818 7.81818L17.0909 9.81818Z" fill="currentColor"/>' |
| 40 |
. '<path d="M17.0909 14.1818L16.1818 16.1818L14.1818 17.0909L16.1818 18L17.0909 20L18 18L20 17.0909L18 16.1818L17.0909 14.1818Z" fill="currentColor"/>' |
| 41 |
. '<path d="M11.6364 10.1818L9.81818 6.18182L8 10.1818L4 12L8 13.8182L9.81818 17.8182L11.6364 13.8182L15.6364 12L11.6364 10.1818ZM10.5382 12.72L9.81818 14.3055L9.09818 12.72L7.51273 12L9.09818 11.28L9.81818 9.69455L10.5382 11.28L12.1236 12L10.5382 12.72Z" fill="currentColor"/>' |
| 42 |
. '</svg>'; |
| 43 |
// phpcs:enable Generic.Files.LineLength.TooLong |
| 44 |
// phpcs:enable PSR12.Properties.ConstantVisibility.NotFound |
| 45 |
|
| 46 |
/** |
| 47 |
* admin_bar_menu fires on template_redirect, too late to gate at hook time. |
| 48 |
* |
| 49 |
* @return void |
| 50 |
*/ |
| 51 |
public static function initAdminBar() |
| 52 |
{ |
| 53 |
// Core's wp-logo node is priority 10, and QuickEdit's toggle rides at 5. |
| 54 |
\add_action('admin_bar_menu', [self::class, 'addAgentButton'], 4); |
| 55 |
} |
| 56 |
|
| 57 |
/** |
| 58 |
* @param \WP_Admin_Bar $bar - The bar being built. |
| 59 |
* |
| 60 |
* @return void |
| 61 |
*/ |
| 62 |
public static function addAgentButton($bar) |
| 63 |
{ |
| 64 |
if (!self::shouldRender() || !\is_user_logged_in()) { |
| 65 |
return; |
| 66 |
} |
| 67 |
|
| 68 |
// buttons.js renders into this same node, so it must not move. |
| 69 |
$bar->add_node([ |
| 70 |
'id' => 'extendify-agent-btn', |
| 71 |
'title' => '<button type="button" class="ext-skeleton-agent-pill" disabled aria-busy="true">' |
| 72 |
. self::MAGIC_ICON |
| 73 |
. '<span>' . \esc_html__('AI Agent', 'extendify-local') . '</span>' |
| 74 |
. '</button>', |
| 75 |
'meta' => ['class' => 'extendify-agent'], |
| 76 |
]); |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* @return bool |
| 81 |
*/ |
| 82 |
protected static function shouldRender() |
| 83 |
{ |
| 84 |
return \wp_is_block_theme() |
| 85 |
&& !\is_customize_preview() |
| 86 |
&& Admin::agentPosition() === 'docked-left'; |
| 87 |
} |
| 88 |
|
| 89 |
/** |
| 90 |
* @return void |
| 91 |
*/ |
| 92 |
public static function init() |
| 93 |
{ |
| 94 |
// agentPosition needs the query, which the constructor is too early for. |
| 95 |
\add_action('wp_head', [self::class, 'printStyles'], 20); |
| 96 |
\add_action('wp_body_open', [self::class, 'printRail'], 1); |
| 97 |
self::initAdminBar(); |
| 98 |
} |
| 99 |
|
| 100 |
/** |
| 101 |
* @return void |
| 102 |
*/ |
| 103 |
public static function printStyles() |
| 104 |
{ |
| 105 |
if (!self::shouldRender()) { |
| 106 |
return; |
| 107 |
} |
| 108 |
|
| 109 |
// phpcs:disable Generic.Files.LineLength.TooLong -- CSS template |
| 110 |
?> |
| 111 |
<style id="extendify-agent-skeleton-styles"> |
| 112 |
#extendify-agent-skeleton, #extendify-agent-skeleton-frame { display: none; } |
| 113 |
@media (min-width: <?php echo (int) self::DESKTOP; ?>px) { |
| 114 |
/* useLayoutShift takes body out of flow, so core's admin-bar |
| 115 |
html margin disappears. */ |
| 116 |
html.extendify-agent-skeleton { margin-top: 0 !important; } |
| 117 |
/* tan(atan2()) is the only way to divide two lengths in CSS. */ |
| 118 |
.extendify-agent-skeleton .wp-site-blocks { |
| 119 |
transform-origin: top left; |
| 120 |
transform: translateX(<?php echo (int) self::WIDTH; ?>px) translateY(<?php echo (int) self::TOP; ?>px) scale(tan(atan2(calc(100vw - <?php echo (int) self::WIDTH; ?>px), 100vw))); |
| 121 |
} |
| 122 |
.extendify-agent-skeleton #wpadminbar { |
| 123 |
margin: <?php echo (int) self::FRAME; ?>px <?php echo (int) self::FRAME; ?>px 0 <?php echo (int) self::WIDTH; ?>px; |
| 124 |
max-width: calc(100% - <?php echo (int) (self::WIDTH + self::FRAME); ?>px); |
| 125 |
border-radius: <?php echo (int) self::FRAME; ?>px <?php echo (int) self::FRAME; ?>px 0 0; |
| 126 |
} |
| 127 |
.extendify-agent-skeleton #extendify-agent-skeleton-frame { |
| 128 |
display: block; |
| 129 |
position: fixed; |
| 130 |
top: <?php echo (int) self::FRAME; ?>px; |
| 131 |
right: <?php echo (int) self::FRAME; ?>px; |
| 132 |
bottom: <?php echo (int) self::FRAME; ?>px; |
| 133 |
left: <?php echo (int) self::WIDTH; ?>px; |
| 134 |
border-radius: 1rem; |
| 135 |
box-shadow: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a, #e0e0e0 0 0 0 9999px; |
| 136 |
pointer-events: none; |
| 137 |
z-index: 100000; |
| 138 |
} |
| 139 |
.extendify-agent-skeleton #extendify-agent-skeleton { |
| 140 |
display: block; |
| 141 |
position: fixed; |
| 142 |
top: 0; |
| 143 |
bottom: 0; |
| 144 |
left: 0; |
| 145 |
width: <?php echo (int) self::WIDTH; ?>px; |
| 146 |
padding: <?php echo (int) self::FRAME; ?>px; |
| 147 |
box-sizing: border-box; |
| 148 |
pointer-events: none; |
| 149 |
/* Underneath the mounted sidebar a held rail would never be seen. */ |
| 150 |
z-index: 1000001; |
| 151 |
} |
| 152 |
/* Only what the mirrored markup has no class for. */ |
| 153 |
#extendify-agent-skeleton .ext-skeleton-icon { height: 24px; width: 24px; } |
| 154 |
#extendify-agent-skeleton textarea { pointer-events: none; } |
| 155 |
#extendify-agent-skeleton .ext-skeleton-bars { |
| 156 |
display: flex; |
| 157 |
align-items: center; |
| 158 |
gap: 4px; |
| 159 |
height: 20px; |
| 160 |
} |
| 161 |
#extendify-agent-skeleton .ext-skeleton-bars span { |
| 162 |
width: 4px; |
| 163 |
height: 6px; |
| 164 |
border-radius: 2px; |
| 165 |
background: #9ca3af; |
| 166 |
animation: ext-skeleton-bars 1.1s ease-in-out infinite; |
| 167 |
} |
| 168 |
#extendify-agent-skeleton .ext-skeleton-bars span:nth-child(2) { animation-delay: 0.15s; } |
| 169 |
#extendify-agent-skeleton .ext-skeleton-bars span:nth-child(3) { animation-delay: 0.3s; } |
| 170 |
@keyframes ext-skeleton-bars { |
| 171 |
0%, 100% { height: 6px; opacity: 0.45; } |
| 172 |
50% { height: 20px; opacity: 1; } |
| 173 |
} |
| 174 |
@media (prefers-reduced-motion: reduce) { |
| 175 |
#extendify-agent-skeleton .ext-skeleton-bars span { height: 12px; animation: none; } |
| 176 |
} |
| 177 |
} |
| 178 |
/* Keeping these after the real button lands would clip it to the 4px stub. */ |
| 179 |
#wpadminbar #wp-admin-bar-extendify-agent-btn:has(.ext-skeleton-agent-pill):not(:has(button:not(.ext-skeleton-agent-pill))) { |
| 180 |
height: 1.75rem; |
| 181 |
margin-inline-end: 4px; |
| 182 |
} |
| 183 |
/* Matches the 4px stub an open panel leaves; dropping it slides the bar 8px. */ |
| 184 |
html.extendify-agent-skeleton #wpadminbar #wp-admin-bar-extendify-agent-btn:has(.ext-skeleton-agent-pill):not(:has(button:not(.ext-skeleton-agent-pill))) { |
| 185 |
width: 4px; |
| 186 |
overflow: hidden; |
| 187 |
} |
| 188 |
#wpadminbar #wp-admin-bar-extendify-agent-btn:has(button:not(.ext-skeleton-agent-pill)) > .ab-item { display: none; } |
| 189 |
/* Sidestep core's .ab-item padding rather than unwinding it. */ |
| 190 |
#wpadminbar #wp-admin-bar-extendify-agent-btn > .ab-item { display: contents; } |
| 191 |
#wpadminbar #wp-admin-bar-extendify-agent-btn .ext-skeleton-agent-pill { |
| 192 |
display: inline-flex; |
| 193 |
align-items: center; |
| 194 |
justify-content: center; |
| 195 |
gap: 4px; |
| 196 |
height: 24px; |
| 197 |
margin: 4px 8px 4px 4px; |
| 198 |
padding: 0 10px; |
| 199 |
border: 0; |
| 200 |
border-radius: 4px; |
| 201 |
background: #3858e9; |
| 202 |
color: #fff; |
| 203 |
font-family: inherit; |
| 204 |
font-size: 13px; |
| 205 |
line-height: 1; |
| 206 |
opacity: 0.6; |
| 207 |
} |
| 208 |
@media (max-width: <?php echo (int) (self::DESKTOP - 1); ?>px) { |
| 209 |
#wpadminbar #wp-admin-bar-extendify-agent-btn:has(.ext-skeleton-agent-pill):not(:has(button:not(.ext-skeleton-agent-pill))) { display: none; } |
| 210 |
} |
| 211 |
</style> |
| 212 |
<?php |
| 213 |
// phpcs:enable Generic.Files.LineLength.TooLong |
| 214 |
self::printGate(); |
| 215 |
} |
| 216 |
|
| 217 |
/** |
| 218 |
* The store persists `open` per site in localStorage, so a closed panel is |
| 219 |
* invisible server-side — and offsetting for one that mounts closed is the |
| 220 |
* jump this class exists to avoid. |
| 221 |
* |
| 222 |
* @return void |
| 223 |
*/ |
| 224 |
protected static function printGate() |
| 225 |
{ |
| 226 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 227 |
$forced = isset($_GET['extendify-launch-success']) || isset($_GET['extendify-open-agent']); |
| 228 |
$siteId = \get_option('extendify_site_id', ''); |
| 229 |
$script = sprintf( |
| 230 |
'try{var r=document.documentElement;' |
| 231 |
. 'var s=%1$s?null:JSON.parse(localStorage.getItem(%2$s));' |
| 232 |
. 'if(!s||!s.state||s.state.open!==false){' |
| 233 |
. 'r.classList.add("extendify-agent-skeleton")}' |
| 234 |
. 'var q=JSON.parse(localStorage.getItem(%3$s));' |
| 235 |
. 'if(q&&q.state&&q.state.on){r.classList.add("extendify-quick-edit-on")}' |
| 236 |
// Left on, it re-offsets the page when the panel closes and clears its styles. |
| 237 |
. 'var t=Date.now();' |
| 238 |
. 'var d=function(){r.classList.remove("extendify-agent-skeleton");' |
| 239 |
. 'var a=document.getElementById("extendify-agent-skeleton");if(a){a.remove()}' |
| 240 |
. 'var b=document.getElementById("extendify-agent-skeleton-frame");if(b){b.remove()}};' |
| 241 |
. 'var i=setInterval(function(){' |
| 242 |
. 'if(!document.getElementById("extendify-agent-sidebar")){return}' |
| 243 |
. 'clearInterval(i);setTimeout(d,Math.max(0,%4$d-(Date.now()-t)))},50);' |
| 244 |
. 'setTimeout(function(){clearInterval(i);d()},10000)}catch(e){}', |
| 245 |
$forced ? 'true' : 'false', |
| 246 |
\wp_json_encode('extendify-agent-global-' . $siteId), |
| 247 |
\wp_json_encode('extendify-quick-edit-mode-' . $siteId), |
| 248 |
self::MIN_VISIBLE |
| 249 |
); |
| 250 |
|
| 251 |
\wp_print_inline_script_tag($script, ['id' => 'extendify-agent-skeleton-gate']); |
| 252 |
} |
| 253 |
|
| 254 |
/** |
| 255 |
* @return void |
| 256 |
*/ |
| 257 |
public static function printRail() |
| 258 |
{ |
| 259 |
if (!self::shouldRender()) { |
| 260 |
return; |
| 261 |
} |
| 262 |
|
| 263 |
// phpcs:disable Generic.Files.LineLength.TooLong -- class strings copied verbatim |
| 264 |
?> |
| 265 |
<div id="extendify-agent-skeleton-frame" aria-hidden="true"></div> |
| 266 |
<div id="extendify-agent-skeleton" class="extendify-agent" aria-hidden="true"> |
| 267 |
<div class="h-full flex flex-col shadow-lg rounded-2xl overflow-hidden bg-white"> |
| 268 |
<div class="group flex shrink-0 items-center justify-between overflow-hidden bg-banner-main text-banner-text"> |
| 269 |
<div class="flex h-full grow items-center justify-between gap-1 p-0 py-2.5"> |
| 270 |
<div class="flex h-5 px-4 max-w-36 overflow-hidden"> |
| 271 |
<img class="max-h-full max-w-full object-contain" src="<?php echo \esc_url(PartnerData::$logo); ?>" alt="<?php echo \esc_attr(PartnerData::$name); ?>" /> |
| 272 |
</div> |
| 273 |
</div> |
| 274 |
<div class="flex gap-1 h-full items-center p-2"> |
| 275 |
<div class="ext-skeleton-icon"></div> |
| 276 |
<div class="ext-skeleton-icon"></div> |
| 277 |
</div> |
| 278 |
</div> |
| 279 |
<div class="relative z-50 flex h-full flex-col justify-between overflow-auto"> |
| 280 |
<div class="relative grow overflow-y-auto overflow-x-hidden p-2 flex items-center justify-center"> |
| 281 |
<div class="ext-skeleton-bars"><span></span><span></span><span></span></div> |
| 282 |
</div> |
| 283 |
<div> |
| 284 |
<div class="relative flex flex-col px-4 pb-2 pt-2.5 shadow-lg-flipped"></div> |
| 285 |
<div class="p-4 pb-2 pt-0"> |
| 286 |
<div class="relative flex w-full flex-col rounded-sm border border-gray-300 bg-gray-50"> |
| 287 |
<textarea class="flex max-h-[calc(75dvh)] min-h-16 w-full resize-none overflow-y-auto bg-transparent px-2 pb-4 pt-2.5 text-base placeholder:text-gray-700 focus:shadow-none focus:outline-hidden disabled:opacity-50 md:text-sm border-none text-gray-900" placeholder="<?php echo \esc_attr__('Ask anything', 'extendify-local'); ?>" rows="1" tabindex="-1" readonly></textarea> |
| 288 |
<div class="flex justify-between gap-4 px-2 pb-2"> |
| 289 |
<div class="ms-auto flex items-center gap-1"> |
| 290 |
<button type="button" class="inline-flex h-7 w-7 items-center justify-center rounded-full border-0 bg-design-main p-0 text-white transition-colors disabled:opacity-20" disabled> |
| 291 |
<svg height="20" width="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 3.9 6.5 9.5l1 1 3.8-3.7V20h1.5V6.8l3.7 3.7 1-1z"></path></svg> |
| 292 |
</button> |
| 293 |
</div> |
| 294 |
</div> |
| 295 |
</div> |
| 296 |
</div> |
| 297 |
<div class="text-pretty px-4 pb-2 text-center text-xss leading-none text-gray-700"> |
| 298 |
<?php echo \esc_html__('AI Agent can make mistakes. Check changes before saving.', 'extendify-local'); ?> |
| 299 |
</div> |
| 300 |
</div> |
| 301 |
</div> |
| 302 |
</div> |
| 303 |
</div> |
| 304 |
<?php |
| 305 |
// phpcs:enable Generic.Files.LineLength.TooLong |
| 306 |
} |
| 307 |
} |
| 308 |
|