PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.6.0 0.7.0 All 126 releases
extendify / app / Agent / Skeleton.php

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

308 lines 14.9 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 /* 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