PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / app / Agent / Skeleton.php

Skeleton.php in Extendify 3.2.2, at app/Agent/Skeleton.php

314 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 /* The mounted Agent drops the page scrollbars; classic ones would shift everything on mount. */
118 html.extendify-agent-skeleton { scrollbar-width: none; }
119 html.extendify-agent-skeleton::-webkit-scrollbar { display: none; }
120 /* tan(atan2()) is the only way to divide two lengths in CSS. */
121 .extendify-agent-skeleton .wp-site-blocks {
122 transform-origin: top left;
123 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)));
124 }
125 .extendify-agent-skeleton #wpadminbar {
126 margin: <?php echo (int) self::FRAME; ?>px <?php echo (int) self::FRAME; ?>px 0 <?php echo (int) self::WIDTH; ?>px;
127 max-width: calc(100% - <?php echo (int) (self::WIDTH + self::FRAME); ?>px);
128 border-radius: <?php echo (int) self::FRAME; ?>px <?php echo (int) self::FRAME; ?>px 0 0;
129 }
130 .extendify-agent-skeleton #extendify-agent-skeleton-frame {
131 display: block;
132 position: fixed;
133 top: <?php echo (int) self::FRAME; ?>px;
134 right: <?php echo (int) self::FRAME; ?>px;
135 bottom: calc(<?php echo (int) self::FRAME; ?>px + var(--extendify-notification-bar-height, 0px));
136 left: <?php echo (int) self::WIDTH; ?>px;
137 border-radius: 1rem;
138 box-shadow: 0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a, #e0e0e0 0 0 0 9999px;
139 /* Unclipped, the 9999px shadow covers the notification bar. */
140 clip-path: inset(-9999px -9999px -<?php echo (int) self::FRAME; ?>px -9999px);
141 pointer-events: none;
142 z-index: 100000;
143 }
144 /* Set by doReload (src/Agent/lib/reload.js) before a reload. */
145 .extendify-agent-skeleton #extendify-agent-skeleton,
146 .extendify-agent-reloading #extendify-agent-skeleton {
147 display: block;
148 position: fixed;
149 top: 0;
150 bottom: var(--extendify-notification-bar-height, 0px);
151 left: 0;
152 width: <?php echo (int) self::WIDTH; ?>px;
153 padding: <?php echo (int) self::FRAME; ?>px;
154 box-sizing: border-box;
155 pointer-events: none;
156 /* Underneath the mounted sidebar a held rail would never be seen. */
157 z-index: 1000001;
158 }
159 /* Only what the mirrored markup has no class for. */
160 #extendify-agent-skeleton .ext-skeleton-icon { height: 24px; width: 24px; }
161 #extendify-agent-skeleton textarea { pointer-events: none; }
162 #extendify-agent-skeleton .ext-skeleton-bars {
163 display: flex;
164 align-items: center;
165 gap: 4px;
166 height: 20px;
167 }
168 #extendify-agent-skeleton .ext-skeleton-bars span {
169 width: 4px;
170 height: 6px;
171 border-radius: 2px;
172 background: #9ca3af;
173 animation: ext-skeleton-bars 1.1s ease-in-out infinite;
174 }
175 #extendify-agent-skeleton .ext-skeleton-bars span:nth-child(2) { animation-delay: 0.15s; }
176 #extendify-agent-skeleton .ext-skeleton-bars span:nth-child(3) { animation-delay: 0.3s; }
177 @keyframes ext-skeleton-bars {
178 0%, 100% { height: 6px; opacity: 0.45; }
179 50% { height: 20px; opacity: 1; }
180 }
181 @media (prefers-reduced-motion: reduce) {
182 #extendify-agent-skeleton .ext-skeleton-bars span { height: 12px; animation: none; }
183 }
184 }
185 /* Keeping these after the real button lands would clip it to the 4px stub. */
186 #wpadminbar #wp-admin-bar-extendify-agent-btn:has(.ext-skeleton-agent-pill):not(:has(button:not(.ext-skeleton-agent-pill))) {
187 height: 1.75rem;
188 margin-inline-end: 4px;
189 }
190 /* Matches the 4px stub an open panel leaves; dropping it slides the bar 8px. */
191 html.extendify-agent-skeleton #wpadminbar #wp-admin-bar-extendify-agent-btn:has(.ext-skeleton-agent-pill):not(:has(button:not(.ext-skeleton-agent-pill))) {
192 width: 4px;
193 overflow: hidden;
194 }
195 #wpadminbar #wp-admin-bar-extendify-agent-btn:has(button:not(.ext-skeleton-agent-pill)) > .ab-item { display: none; }
196 /* Sidestep core's .ab-item padding rather than unwinding it. */
197 #wpadminbar #wp-admin-bar-extendify-agent-btn > .ab-item { display: contents; }
198 #wpadminbar #wp-admin-bar-extendify-agent-btn .ext-skeleton-agent-pill {
199 display: inline-flex;
200 align-items: center;
201 justify-content: center;
202 gap: 4px;
203 height: 24px;
204 margin: 4px 8px 4px 4px;
205 padding: 0 10px;
206 border: 0;
207 border-radius: 4px;
208 background: #3858e9;
209 color: #fff;
210 font-family: inherit;
211 font-size: 13px;
212 line-height: 1;
213 opacity: 0.6;
214 }
215 @media (max-width: <?php echo (int) (self::DESKTOP - 1); ?>px) {
216 #wpadminbar #wp-admin-bar-extendify-agent-btn:has(.ext-skeleton-agent-pill):not(:has(button:not(.ext-skeleton-agent-pill))) { display: none; }
217 }
218 </style>
219 <?php
220 // phpcs:enable Generic.Files.LineLength.TooLong
221 self::printGate();
222 }
223
224 /**
225 * The store persists `open` per site in localStorage, so a closed panel is
226 * invisible server-side — and offsetting for one that mounts closed is the
227 * jump this class exists to avoid.
228 *
229 * @return void
230 */
231 protected static function printGate()
232 {
233 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
234 $forced = isset($_GET['extendify-launch-success']) || isset($_GET['extendify-open-agent']);
235 $siteId = \get_option('extendify_site_id', '');
236 $script = sprintf(
237 'try{var r=document.documentElement;'
238 . 'var s=%1$s?null:JSON.parse(localStorage.getItem(%2$s));'
239 . 'if(!s||!s.state||s.state.open!==false){'
240 . 'r.classList.add("extendify-agent-skeleton")}'
241 . 'var q=JSON.parse(localStorage.getItem(%3$s));'
242 . 'if(q&&q.state&&q.state.on){r.classList.add("extendify-quick-edit-on")}'
243 // Left on, it re-offsets the page when the panel closes and clears its styles.
244 . 'var t=Date.now();'
245 // Only hides the rail; doReload shows it again before a reload.
246 . 'var d=function(){r.classList.remove("extendify-agent-skeleton")};'
247 . 'var i=setInterval(function(){'
248 . 'if(!document.getElementById("extendify-agent-sidebar")){return}'
249 . 'clearInterval(i);setTimeout(d,Math.max(0,%4$d-(Date.now()-t)))},50);'
250 . 'setTimeout(function(){clearInterval(i);d()},10000)}catch(e){}',
251 $forced ? 'true' : 'false',
252 \wp_json_encode('extendify-agent-global-' . $siteId),
253 \wp_json_encode('extendify-quick-edit-mode-' . $siteId),
254 self::MIN_VISIBLE
255 );
256
257 \wp_print_inline_script_tag($script, ['id' => 'extendify-agent-skeleton-gate']);
258 }
259
260 /**
261 * @return void
262 */
263 public static function printRail()
264 {
265 if (!self::shouldRender()) {
266 return;
267 }
268
269 // phpcs:disable Generic.Files.LineLength.TooLong -- class strings copied verbatim
270 ?>
271 <div id="extendify-agent-skeleton-frame" aria-hidden="true"></div>
272 <div id="extendify-agent-skeleton" class="extendify-agent" aria-hidden="true">
273 <div class="h-full flex flex-col shadow-lg rounded-2xl overflow-hidden bg-white">
274 <div class="group flex shrink-0 items-center justify-between overflow-hidden bg-banner-main text-banner-text">
275 <div class="flex h-full grow items-center justify-between gap-1 p-0 py-2.5">
276 <div class="flex h-5 px-4 max-w-36 overflow-hidden">
277 <img class="max-h-full max-w-full object-contain" src="<?php echo \esc_url(PartnerData::$logo); ?>" alt="<?php echo \esc_attr(PartnerData::$name); ?>" />
278 </div>
279 </div>
280 <div class="flex gap-1 h-full items-center p-2">
281 <div class="ext-skeleton-icon"></div>
282 <div class="ext-skeleton-icon"></div>
283 </div>
284 </div>
285 <div class="relative z-50 flex h-full flex-col justify-between overflow-auto">
286 <div class="relative grow overflow-y-auto overflow-x-hidden p-2 flex items-center justify-center" style="scrollbar-gutter: stable">
287 <div class="ext-skeleton-bars"><span></span><span></span><span></span></div>
288 </div>
289 <div>
290 <div class="relative flex flex-col px-4 pb-2 pt-2.5 shadow-lg-flipped"></div>
291 <div class="p-4 pb-2 pt-0">
292 <div class="relative flex w-full flex-col rounded-sm border border-gray-300 bg-gray-50">
293 <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>
294 <div class="flex justify-between gap-4 px-2 pb-2">
295 <div class="ms-auto flex items-center gap-1">
296 <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>
297 <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>
298 </button>
299 </div>
300 </div>
301 </div>
302 </div>
303 <div class="text-pretty px-4 pb-2 text-center text-xss leading-none text-gray-700">
304 <?php echo \esc_html__('AI Agent can make mistakes. Check changes before saving.', 'extendify-local'); ?>
305 </div>
306 </div>
307 </div>
308 </div>
309 </div>
310 <?php
311 // phpcs:enable Generic.Files.LineLength.TooLong
312 }
313 }
314