PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / gutenberg / ai-shared / kit.js

kit.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/gutenberg/ai-shared/kit.js

338 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ===== ai-shared/kit.js — shared primitives for the BetterDocs AI surfaces =====
2 // Ported from the AI Studio mockup (ai-kit.jsx). Plain-React components styled
3 // with inline styles + the CSS custom properties defined in tokens.scss. Every
4 // portal root carries `.bd-ai-scope` so those `var(--x)` tokens resolve.
5 //
6 // Consumed by BOTH the Write-with-AI modal and the restyled Edit-with-AI modal.
7
8 import { useState, useEffect, useRef, createPortal } from "@wordpress/element";
9 import Select from "react-select";
10
11 // ── Icons (stroke 1.7, currentColor — same convention as the mockup) ─────────
12 export const AIcon = ({ d, size = 16, fill = "none", stroke = "currentColor", sw = 1.7, children, ...p }) => (
13 <svg width={size} height={size} viewBox="0 0 24 24" fill={fill} stroke={stroke}
14 strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" {...p}>
15 {d && <path d={d} />}
16 {children}
17 </svg>
18 );
19
20 export const IcClose = (p) => <AIcon {...p} d="M6 6l12 12M6 18 18 6" />;
21 export const IcChevDown = (p) => <AIcon {...p} d="m6 9 6 6 6-6" />;
22 export const IcPlus = (p) => <AIcon {...p} d="M12 5v14M5 12h14" />;
23 export const IcTrash = (p) => <AIcon {...p}><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" /></AIcon>;
24 export const IcGrip = (p) => (
25 <AIcon {...p} fill="currentColor" stroke="none">
26 <circle cx="9" cy="6" r="1.4" /><circle cx="9" cy="12" r="1.4" /><circle cx="9" cy="18" r="1.4" />
27 <circle cx="15" cy="6" r="1.4" /><circle cx="15" cy="12" r="1.4" /><circle cx="15" cy="18" r="1.4" />
28 </AIcon>
29 );
30 export const IcCheck = (p) => <AIcon {...p} d="M5 12l5 5L20 7" />;
31 export const IcArrowR = (p) => <AIcon {...p} d="M5 12h14M13 5l7 7-7 7" />;
32 export const IcRefresh = (p) => <AIcon {...p} d="M21 12a9 9 0 1 1-3.51-7.13M21 4v5h-5" />;
33 export const IcAlert = (p) => <AIcon {...p}><circle cx="12" cy="12" r="10" /><path d="M12 8v5M12 16.5h.01" /></AIcon>;
34 export const IcList = (p) => <AIcon {...p} d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" />;
35 export const IcSummary = (p) => <AIcon {...p} d="M4 6h16M4 10h10M4 14h16M4 18h7" />;
36
37 // BetterDocs AI mark — the accent-colored pencil from the modal header
38 export const AIMark = ({ size = 18 }) => (
39 <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
40 stroke="var(--ai)" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
41 <path d="M12 20h9" />
42 <path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
43 </svg>
44 );
45 export const AIMarkWhite = ({ size = 16 }) => (
46 <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="white"
47 strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
48 <path d="M12 20h9" />
49 <path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
50 </svg>
51 );
52 export const AIBadgeIcon = ({ size = 14 }) => (
53 <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" stroke="none">
54 <path d="M12 2l1.9 5.1L19 9l-5.1 1.9L12 16l-1.9-5.1L5 9l5.1-1.9z" />
55 <path d="M19 14l.9 2.4L22 17l-2.1.6L19 20l-.9-2.4L16 17l2.1-.6z" />
56 </svg>
57 );
58
59 // ── AI icon disc (header) ────────────────────────────────────────────────────
60 export const AIDisc = ({ size = 44 }) => (
61 <div style={{
62 width: size, height: size, borderRadius: "50%",
63 background: "var(--ai-50)", flexShrink: 0,
64 display: "flex", alignItems: "center", justifyContent: "center",
65 }}>
66 <AIMark size={size * 0.42} />
67 </div>
68 );
69
70 // ── Buttons ──────────────────────────────────────────────────────────────────
71 export const PrimaryBtn = ({ children, icon, onClick, disabled, style }) => (
72 <button type="button" onClick={disabled ? undefined : onClick} disabled={disabled} style={{
73 display: "inline-flex", alignItems: "center", gap: 8,
74 background: disabled ? "#cbd5e1" : "var(--green)", color: "white",
75 padding: "11px 20px", borderRadius: 8, border: "none",
76 fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
77 cursor: disabled ? "not-allowed" : "pointer",
78 boxShadow: disabled ? "none" : "0 1px 0 rgba(0,0,0,.04), inset 0 -1px 0 rgba(0,0,0,.06)",
79 transition: "background .15s, transform .05s",
80 ...style,
81 }}
82 onMouseEnter={e => { if (!disabled) e.currentTarget.style.background = "var(--green-600)"; }}
83 onMouseLeave={e => { if (!disabled) e.currentTarget.style.background = "var(--green)"; }}>
84 {icon}{children}
85 </button>
86 );
87
88 // Outlined accent button — uses the AI accent color
89 export const AccentBtn = ({ children, icon, onClick, disabled, style }) => (
90 <button type="button" onClick={disabled ? undefined : onClick} disabled={disabled} style={{
91 display: "inline-flex", alignItems: "center", gap: 8,
92 background: "white",
93 color: disabled ? "var(--muted-2)" : "var(--ai)",
94 padding: "11px 18px", borderRadius: 8,
95 border: `1.5px solid ${disabled ? "var(--border)" : "var(--ai-100)"}`,
96 fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
97 cursor: disabled ? "not-allowed" : "pointer",
98 transition: "background .15s, border-color .15s, color .15s",
99 ...style,
100 }}
101 onMouseEnter={e => { if (!disabled) { e.currentTarget.style.background = "var(--ai-50)"; e.currentTarget.style.borderColor = "var(--ai)"; } }}
102 onMouseLeave={e => { if (!disabled) { e.currentTarget.style.background = "white"; e.currentTarget.style.borderColor = "var(--ai-100)"; } }}>
103 {icon}{children}
104 </button>
105 );
106
107 export const GhostBtn = ({ children, icon, onClick, style, disabled }) => (
108 <button type="button" onClick={disabled ? undefined : onClick} disabled={disabled} style={{
109 display: "inline-flex", alignItems: "center", gap: 8,
110 background: "white", color: disabled ? "var(--muted-2)" : "var(--ink-2)",
111 padding: "10px 16px", borderRadius: 8,
112 border: "1px solid var(--border)",
113 fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
114 cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.7 : 1,
115 transition: "background .15s, border-color .15s",
116 ...style,
117 }}
118 onMouseEnter={e => { if (!disabled) { e.currentTarget.style.background = "#f8fafc"; e.currentTarget.style.borderColor = "#d8dde3"; } }}
119 onMouseLeave={e => { if (!disabled) { e.currentTarget.style.background = "white"; e.currentTarget.style.borderColor = "var(--border)"; } }}>
120 {icon}{children}
121 </button>
122 );
123
124 // Quiet tertiary text button — used for "Discard"
125 export const TextBtn = ({ children, onClick, tone = "muted", style }) => {
126 const color = tone === "danger" ? "#dc2626" : "var(--muted)";
127 return (
128 <button type="button" onClick={onClick} style={{
129 display: "inline-flex", alignItems: "center", gap: 6,
130 background: "transparent", color, padding: "10px 12px",
131 border: "none", borderRadius: 8, fontSize: 13.5, fontWeight: 600, cursor: "pointer",
132 transition: "background .12s, color .12s", ...style,
133 }}
134 onMouseEnter={e => { e.currentTarget.style.background = tone === "danger" ? "#fef2f2" : "#f4f6f8"; e.currentTarget.style.color = tone === "danger" ? "#dc2626" : "var(--ink-2)"; }}
135 onMouseLeave={e => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = color; }}>
136 {children}
137 </button>
138 );
139 };
140
141 // ── "AI generated" pill ──────────────────────────────────────────────────────
142 // Neutral ash pill so the surface isn't flooded with green — the AI identity is
143 // carried by the small green sparkle icon alone.
144 export const AIPill = ({ children = "AI generated", style }) => (
145 <span style={{
146 display: "inline-flex", alignItems: "center", gap: 6,
147 background: "var(--text-color-50, #f9fafb)", color: "var(--text-color-600, #667085)",
148 padding: "5px 11px 5px 9px", borderRadius: 999,
149 fontSize: 11.5, fontWeight: 700, letterSpacing: 0.1, whiteSpace: "nowrap", flexShrink: 0,
150 border: "1px solid var(--text-color-200, #eaecf0)", ...style,
151 }}>
152 <span style={{ color: "var(--ai)", display: "flex" }}><AIBadgeIcon size={13} /></span>{children}
153 </span>
154 );
155
156 // ── WP-style notice banner (light red, left accent bar) ──────────────────────
157 export const Banner = ({ tone = "error", children, style }) => {
158 const t = tone === "error"
159 ? { bg: "var(--wp-red-50)", bar: "var(--wp-red)", fg: "#8a1f20", icon: "var(--wp-red)" }
160 : { bg: "#fef6e0", bar: "#d9a406", fg: "#8a6400", icon: "#d9a406" };
161 return (
162 <div style={{
163 display: "flex", alignItems: "center", gap: 10,
164 background: t.bg, color: t.fg,
165 borderRadius: 8,
166 padding: "12px 14px", fontSize: 13, fontWeight: 500, lineHeight: 1.5,
167 ...style,
168 }}>
169 <span style={{ color: t.icon, flexShrink: 0, display: "flex" }}><IcAlert size={17} sw={2} /></span>
170 <span style={{ flex: 1 }}>{children}</span>
171 </div>
172 );
173 };
174
175 // ── Spinner + inline "working" status ────────────────────────────────────────
176 export const Spinner = ({ size = 18, color = "var(--ai)", sw = 2.4 }) => (
177 <svg width={size} height={size} viewBox="0 0 24 24" fill="none" style={{ animation: "bd-ai-spin .8s linear infinite", flexShrink: 0 }}>
178 <circle cx="12" cy="12" r="9" stroke="currentColor" strokeOpacity=".18" strokeWidth={sw} style={{ color }} />
179 <path d="M21 12a9 9 0 0 0-9-9" stroke={color} strokeWidth={sw} strokeLinecap="round" />
180 </svg>
181 );
182 export const WorkingDots = ({ color = "var(--ai)" }) => (
183 <span style={{ display: "inline-flex", gap: 3, marginLeft: 1 }}>
184 {[0, 1, 2].map(i => (
185 <span key={i} style={{
186 width: 4, height: 4, borderRadius: "50%", background: color,
187 animation: "bd-ai-dotPulse 1.1s ease-in-out infinite", animationDelay: `${i * 0.16}s`,
188 }} />
189 ))}
190 </span>
191 );
192
193 // ── Inputs ───────────────────────────────────────────────────────────────────
194 export const FieldLabel = ({ children, required, style }) => (
195 <div style={{ fontSize: 13, fontWeight: 700, color: "var(--ink-2)", marginBottom: 8, ...style }}>
196 {children}{required && <span style={{ color: "#ef4444", marginLeft: 3 }}>*</span>}
197 </div>
198 );
199
200 export const TextInput = ({ value, onChange, placeholder, autoFocus, disabled }) => {
201 const [focus, setFocus] = useState(false);
202 return (
203 <input
204 type="text" autoFocus={autoFocus} value={value} placeholder={placeholder} disabled={disabled}
205 onChange={(e) => onChange(e.target.value)}
206 onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
207 style={{
208 width: "100%", border: `1px solid ${focus ? "var(--green)" : "var(--border)"}`,
209 background: focus ? "white" : "#fcfcfd",
210 boxShadow: focus ? "0 0 0 3px rgba(0,184,132,.14)" : "none",
211 borderRadius: 10, padding: "12px 14px", fontSize: 14, color: "var(--ink)",
212 outline: "none", transition: "border-color .15s, box-shadow .15s, background .15s",
213 }} />
214 );
215 };
216
217 export const TextArea = ({ value, onChange, placeholder, rows = 4, disabled }) => {
218 const [focus, setFocus] = useState(false);
219 return (
220 <textarea
221 rows={rows} value={value} placeholder={placeholder} disabled={disabled}
222 onChange={(e) => onChange(e.target.value)}
223 onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
224 style={{
225 width: "100%", resize: "vertical",
226 border: `1px solid ${focus ? "var(--green)" : "var(--border)"}`,
227 background: focus ? "white" : "#fcfcfd",
228 boxShadow: focus ? "0 0 0 3px rgba(0,184,132,.14)" : "none",
229 borderRadius: 10, padding: "12px 14px", fontSize: 14, color: "var(--ink-2)",
230 outline: "none", fontFamily: "inherit", lineHeight: 1.55,
231 transition: "border-color .15s, box-shadow .15s, background .15s",
232 }} />
233 );
234 };
235
236 // On/off switch styled to match the kit — used for the title / overwrite toggles.
237 export const Toggle = ({ checked, onChange, label, disabled }) => (
238 <label style={{
239 display: "inline-flex", alignItems: "center", gap: 9,
240 fontSize: 12.5, fontWeight: 600, color: "var(--ink-2)",
241 cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.6 : 1,
242 }}>
243 <span
244 role="switch" aria-checked={!!checked}
245 onClick={() => { if (!disabled) onChange(!checked); }}
246 style={{
247 position: "relative", width: 36, height: 20, flexShrink: 0,
248 borderRadius: 999, background: checked ? "var(--green)" : "#cbd5e1",
249 transition: "background .15s",
250 }}>
251 <span style={{
252 position: "absolute", top: 2, left: checked ? 18 : 2,
253 width: 16, height: 16, borderRadius: "50%", background: "white",
254 boxShadow: "0 1px 2px rgba(15,23,42,.25)", transition: "left .15s",
255 }} />
256 </span>
257 {label}
258 </label>
259 );
260
261 // Native <select> styled to match the BetterDocs dashboard select control via
262 // the shared `.bd-ai-select` class (see ai-shared/tokens.scss). Used for the
263 // language / source-type / git / tone / length pickers.
264 export const Pick = ({ value, onChange, options, style, disabled }) => (
265 <div style={{ position: "relative", ...style }}>
266 <select className="bd-ai-select" value={value} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
267 {options.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
268 </select>
269 <span style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", pointerEvents: "none", color: "var(--muted)", display: "flex" }}>
270 <IcChevDown size={16} />
271 </span>
272 </div>
273 );
274
275 // react-select dropdown that reuses the BetterDocs dashboard "betterdocs-dropdown"
276 // (FAQ order / bulk-actions) markup + classNamePrefix, so its control + styled
277 // option menu match the admin exactly. Styling lives in ai-shared/tokens.scss
278 // (scoped to .bd-ai-scope). Used for the source / git / language pickers.
279 export const DropdownSelect = ({ value, onChange, options, style, disabled, isSearchable = false, placeholder, formatOptionLabel }) => (
280 <div className="betterdocs-dropdown-wrapper" style={style}>
281 <Select
282 className="betterdocs-dropdown-select"
283 classNamePrefix="betterdocs-dropdown"
284 value={options.find((o) => o.value === value) || null}
285 options={options}
286 onChange={(opt) => onChange(opt ? opt.value : "")}
287 isDisabled={disabled}
288 isSearchable={isSearchable}
289 isClearable={false}
290 placeholder={placeholder}
291 formatOptionLabel={formatOptionLabel}
292 menuPlacement="auto"
293 menuShouldScrollIntoView={false}
294 />
295 </div>
296 );
297
298 // ── Modal shell — rendered through a portal at document.body ──────────────────
299 export const Modal = ({ open, onClose, width = 600, children, labelledBy }) => {
300 useEffect(() => {
301 if (!open) return;
302 const onKey = (e) => { if (e.key === "Escape") onClose(); };
303 document.addEventListener("keydown", onKey);
304 return () => document.removeEventListener("keydown", onKey);
305 }, [open, onClose]);
306 if (!open) return null;
307 return createPortal(
308 <div className="bd-ai-scope" style={{ position: "fixed", inset: 0, zIndex: 100050 }} role="dialog" aria-modal="true" aria-labelledby={labelledBy}>
309 <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(15,23,42,.4)", animation: "bd-ai-fadeIn .15s ease" }} />
310 <div style={{
311 position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)",
312 width, maxWidth: "calc(100vw - 48px)", maxHeight: "calc(100vh - 48px)",
313 background: "white", borderRadius: 16, boxShadow: "var(--shadow-lg)",
314 display: "flex", flexDirection: "column", overflow: "hidden",
315 animation: "bd-ai-modalIn .2s cubic-bezier(.2,.7,.3,1)",
316 }}>
317 {children}
318 {/* Matches the admin .bd-faq-modal-close-button — 32px ash rounded
319 square with the shared X glyph, no shadow. */}
320 <button type="button" onClick={onClose} title="Close" aria-label="Close" style={{
321 position: "absolute", top: 16, right: 22, zIndex: 2,
322 width: 32, height: 32, borderRadius: 9, border: "none",
323 display: "inline-flex", alignItems: "center", justifyContent: "center",
324 color: "var(--muted)", background: "#f2f4f7", cursor: "pointer",
325 boxShadow: "none", transition: "all .2s ease-in-out",
326 }}
327 onMouseEnter={(e) => { e.currentTarget.style.background = "#eaecf0"; e.currentTarget.style.color = "var(--ink)"; }}
328 onMouseLeave={(e) => { e.currentTarget.style.background = "#f2f4f7"; e.currentTarget.style.color = "var(--muted)"; }}>
329 <svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
330 <path d="M1.69687 14L0.296875 12.6L5.89687 7L0.296875 1.4L1.69687 0L7.29688 5.6L12.8969 0L14.2969 1.4L8.69688 7L14.2969 12.6L12.8969 14L7.29688 8.4L1.69687 14Z" fill="currentColor" />
331 </svg>
332 </button>
333 </div>
334 </div>,
335 document.body
336 );
337 };
338