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 / admin / sample-docs / Pieces.js

Pieces.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/sample-docs/Pieces.js

488 lines 24.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React from "react";
2 import { useState, useEffect, useRef } from "@wordpress/element";
3 import { __, _n, sprintf } from "@wordpress/i18n";
4 import { decodeEntities } from "@wordpress/html-entities";
5 import { Sparkle, Spinner, IconClose, IconCheck, IconDoc } from "./icons";
6 import { IconDrag } from "../../icons";
7
8 /* ---------- AI badge ---------- */
9 export const AiBadge = ({ label }) => (
10 <span className="betterdocs-sd-badge">
11 <Sparkle size={14} /> {label || __("AI-powered — no API key needed", "betterdocs")}
12 </span>
13 );
14
15 /* ---------- "Generate Sample Docs" sparkle pill ---------- */
16 export const GenerateBtn = ({ children, onClick, disabled, block }) => (
17 <button
18 type="button"
19 className={`betterdocs-sd-gen-btn${block ? " betterdocs-sd-gen-btn--block" : ""}`}
20 onClick={disabled ? undefined : onClick}
21 disabled={disabled}
22 >
23 <Sparkle size={16} /> {children || __("Generate Sample Docs", "betterdocs")}
24 </button>
25 );
26
27 /* ---------- Detecting view ---------- */
28 export const DetectingView = ({ title, sub }) => (
29 <div className="betterdocs-sd-detecting betterdocs-sd-fade">
30 <div className="betterdocs-sd-detecting-orb">
31 <Spinner size={64} />
32 <span className="betterdocs-sd-detecting-spark"><Sparkle size={24} /></span>
33 </div>
34 <div className="betterdocs-sd-detecting-title">
35 {title || __("Analyzing your site…", "betterdocs")}
36 </div>
37 <div className="betterdocs-sd-detecting-sub">
38 {sub || __("Detecting your site type and the topics your visitors ask about.", "betterdocs")}
39 </div>
40 <div className="betterdocs-sd-detecting-dots">
41 {[0, 1, 2].map((i) => (
42 <span key={i} style={{ animationDelay: `${i * 0.18}s` }} />
43 ))}
44 </div>
45 </div>
46 );
47
48 /* ---------- Editable category chips ---------- */
49 export function CategoryChips({ cats, setCats, emptyText }) {
50 const remove = (id) => setCats((cs) => cs.filter((c) => c.id !== id));
51
52 if (!cats.length) {
53 return (
54 <p className="betterdocs-sd-chips-empty">
55 {emptyText || __("All suggestions removed. Reload this page to start over with a fresh set.", "betterdocs")}
56 </p>
57 );
58 }
59
60 return (
61 <div className="betterdocs-sd-chips">
62 {cats.map((c) => (
63 <span key={c.id} className="betterdocs-sd-chip betterdocs-sd-pop">
64 {decodeEntities(c.name)}
65 <button
66 type="button"
67 className="betterdocs-sd-chip-remove"
68 title={__("Remove", "betterdocs")}
69 onClick={() => remove(c.id)}
70 >
71 <IconClose size={14} sw={2.4} />
72 </button>
73 </span>
74 ))}
75 </div>
76 );
77 }
78
79 /* ---------- Quota note ---------- */
80 export const QuotaNote = () => (
81 <div className="betterdocs-sd-quota betterdocs-sd-fade">
82 <span className="betterdocs-sd-quota-icon"><Sparkle size={16} /></span>
83 <div className="betterdocs-sd-quota-text">
84 {__("You've used your free AI generation. Add your OpenAI API key in Settings to generate more — your suggested categories are saved below.", "betterdocs")}
85 </div>
86 </div>
87 );
88
89 /* ---------- Detected profile ---------- */
90 export function ProfileBody({ profile, cats, setCats, quota, contentType = "docs", intent = "", setIntent }) {
91 const site = profile?.site || {};
92 // Prefer the server's content-aware niche label (e.g. "AI / LLM platform");
93 // fall back to the plugin-type label for older profiles.
94 const typeLabel = decodeEntities((profile?.niche_label && String(profile.niche_label)) || humanType(profile?.type));
95 const signals = (profile?.signals?.active_plugins || []).slice(0, 5);
96 const storeName = decodeEntities(site.title || __("your site", "betterdocs"));
97 // Content-type-specific scan blurb: docs vs FAQ vs WooCommerce product FAQ.
98 const blurb =
99 contentType === "product_faq"
100 ? __("We scanned %s and prepared starter FAQs for your product categories, from what your products actually say.", "betterdocs")
101 : contentType === "faq"
102 ? __("We scanned %s and prepared a starter list of FAQs tailored to what your visitors usually ask.", "betterdocs")
103 : __("We scanned %s and prepared a starter knowledge base tailored to what your visitors usually ask.", "betterdocs");
104 // Both FAQ tabs now work the same way: real detected topics as chips (the General
105 // tab's site subjects, the WooCommerce tab's product categories), with the AI
106 // designing the groups and writing the questions. Only docs differs.
107 const isGeneralFaq = contentType !== "docs";
108 const showIntent = !!setIntent && (contentType === "docs" || isGeneralFaq);
109 const intentCopy = isGeneralFaq
110 ? contentType === "product_faq"
111 ? {
112 label: __("What should these product FAQs answer? (optional)", "betterdocs"),
113 placeholder: __("e.g. Sizing and care questions shoppers ask before buying our apparel.", "betterdocs"),
114 hint: __("BetterDocs AI writes an FAQ for each product category, from your real products — and attaches it to that category so it shows on those product pages.", "betterdocs"),
115 }
116 : {
117 label: __("What should these FAQs answer? (optional)", "betterdocs"),
118 placeholder: __("e.g. Questions advertisers ask before signing up — pricing, billing, campaign limits.", "betterdocs"),
119 hint: __("BetterDocs AI groups the questions by subject the way a real FAQ page does, and answers each in a few sentences. Add a line to steer it.", "betterdocs"),
120 }
121 : {
122 label: __("What should this knowledge base be about? (optional)", "betterdocs"),
123 placeholder: __("e.g. A knowledge base for our billboard advertising platform, for both advertisers and operators.", "betterdocs"),
124 hint: __("BetterDocs AI designs the full structure — an Introduction, a Quickstart, and in-depth guides — from your site content. Add a sentence to steer it.", "betterdocs"),
125 };
126 // Count of detected subjects (chips). Docs used to also show an article count
127 // ("3 categories, 9 docs"), but those were placeholder seed articles — the deep
128 // outline flow right-sizes the real KB, so the number never matched. All three tabs
129 // now show just the detected-topic count.
130 const catCount = sprintf(_n("%d topic", "%d topics", cats.length, "betterdocs"), cats.length);
131
132 return (
133 <div className="betterdocs-sd-profile betterdocs-sd-fade">
134 <div>
135 <div className="betterdocs-sd-profile-head">
136 <h3 className="betterdocs-sd-profile-title">
137 {sprintf(__("We detected %s", "betterdocs"), withArticle(typeLabel))}
138 </h3>
139 </div>
140 <p className="betterdocs-sd-profile-blurb">
141 {sprintf(blurb, storeName)}
142 </p>
143 {signals.length > 0 && (
144 <div className="betterdocs-sd-signals">
145 {signals.map((s) => (
146 <span key={s} className="betterdocs-sd-signal">
147 <span className="betterdocs-sd-signal-dot" />
148 {s}
149 </span>
150 ))}
151 </div>
152 )}
153 </div>
154
155 {showIntent && (
156 <div className="betterdocs-sd-intent">
157 <label className="betterdocs-sd-intent-label" htmlFor="betterdocs-sd-intent-input">
158 {intentCopy.label}
159 </label>
160 <textarea
161 id="betterdocs-sd-intent-input"
162 className="betterdocs-sd-intent-input"
163 rows={2}
164 maxLength={400}
165 placeholder={intentCopy.placeholder}
166 value={intent}
167 onChange={(e) => setIntent(e.target.value)}
168 />
169 <p className="betterdocs-sd-intent-hint">{intentCopy.hint}</p>
170 </div>
171 )}
172
173 <div>
174 {/* Hide the whole "Topics we detected" section when there are no chips —
175 the same for docs, sample FAQ and sample product FAQ. An empty state
176 ("No specific topics detected" / "All suggestions removed") reads as a
177 failure, but it isn't: the AI still designs the content from the full
178 site content digest. So we omit the section and let the intent box +
179 generation speak for it. */}
180 {cats.length > 0 && (
181 <>
182 <div className="betterdocs-sd-profile-subhead">
183 <span className="betterdocs-sd-profile-subtitle">
184 {__("Topics we detected", "betterdocs")}
185 </span>
186 <span className="betterdocs-sd-profile-count">
187 {catCount}
188 </span>
189 </div>
190 <CategoryChips cats={cats} setCats={setCats} />
191 <p className="betterdocs-sd-intent-hint">
192 {contentType === "product_faq"
193 ? __("These are your product categories. BetterDocs AI writes an FAQ for each one from your real products — remove a category to keep it out.", "betterdocs")
194 : contentType === "docs"
195 ? __("These are subjects found on your site. BetterDocs AI designs your knowledge base around them — remove a topic to keep it out.", "betterdocs")
196 : __("These are subjects found on your site. BetterDocs AI designs the FAQ groups, writes the questions and answers them — remove a topic to keep it out.", "betterdocs")}
197 </p>
198 </>
199 )}
200 <p className="betterdocs-sd-privacy-note">
201 {contentType === "product_faq"
202 ? __("We send your site type and category suggestions to BetterDocs AI. No customer personal data is included.", "betterdocs")
203 : __("We send your site type, detected topics and short public content excerpts to BetterDocs AI. No customer personal data is included.", "betterdocs")}
204 </p>
205 </div>
206
207 {quota && <QuotaNote />}
208 </div>
209 );
210 }
211
212 /* ---------- Generating progress ---------- */
213 export function useGenProgress(cats, ready, onDone) {
214 const [rows, setRows] = useState(() => cats.map((c) => ({ id: c.id, state: "pending", written: 0 })));
215 const [pct, setPct] = useState(0);
216 const [seqDone, setSeqDone] = useState(false); // the "writing" animation has played through every category
217 const doneRef = useRef(onDone);
218 doneRef.current = onDone;
219 const finished = useRef(false);
220
221 // The writing animation fills the bar the whole way; once it reaches 100% the
222 // "Finalizing" step (below) covers the wait for the real generation result.
223 const CAP = 100;
224
225 // Category "writing" animation — runs once on mount, drives the bar to CAP.
226 useEffect(() => {
227 let alive = true;
228 const timers = [];
229 const after = (ms, fn) => {
230 const t = setTimeout(() => alive && fn(), ms);
231 timers.push(t);
232 };
233 setRows(cats.map((c) => ({ id: c.id, state: "pending", written: 0 })));
234 setPct(0);
235 setSeqDone(false);
236 finished.current = false;
237 const total = cats.length || 1;
238
239 const runCat = (i) => {
240 if (i >= cats.length) {
241 setSeqDone(true);
242 return;
243 }
244 setRows((r) => r.map((x, idx) => (idx === i ? { ...x, state: "creating" } : x)));
245 after(480, () => {
246 setRows((r) => r.map((x, idx) => (idx === i ? { ...x, state: "writing" } : x)));
247 const arts = cats[i].articles.length;
248 // FAQ topics carry no seeded questions (the AI decides how many each
249 // subject warrants), so there is nothing to count up — hold the row in
250 // "writing" for a beat, then mark it done.
251 if (!arts) {
252 after(900, () => {
253 setRows((r) => r.map((x, idx) => (idx === i ? { ...x, state: "done" } : x)));
254 setPct(Math.round(((i + 1) / total) * CAP));
255 after(280, () => runCat(i + 1));
256 });
257 return;
258 }
259 const writeOne = (w) => {
260 setRows((r) => r.map((x, idx) => (idx === i ? { ...x, written: w } : x)));
261 if (w < arts) after(300, () => writeOne(w + 1));
262 else
263 after(360, () => {
264 setRows((r) => r.map((x, idx) => (idx === i ? { ...x, state: "done" } : x)));
265 setPct(Math.round(((i + 1) / total) * CAP));
266 after(280, () => runCat(i + 1));
267 });
268 };
269 after(320, () => writeOne(1));
270 });
271 };
272 after(450, () => runCat(0));
273 return () => {
274 alive = false;
275 timers.forEach(clearTimeout);
276 };
277 }, []);
278
279 // Completion: once the writing animation has played through (bar at 100%),
280 // advance to the preview the instant the real result is ready. While waiting,
281 // the bar rests at 100% and the "Finalizing" step shows a spinner.
282 useEffect(() => {
283 if (finished.current || !seqDone || !ready) return;
284 finished.current = true;
285 setPct(100);
286 const t = setTimeout(() => doneRef.current && doneRef.current(), 300);
287 return () => clearTimeout(t);
288 }, [seqDone, ready]);
289
290 // `finalizing`: the writing animation has played through (bar at 100%) but the
291 // real generation result hasn't arrived yet.
292 // `done`: the animation finished AND the real result is ready — the terminal
293 // 100% state, just before we transition to the preview.
294 return { rows, pct, finalizing: seqDone && !ready, done: seqDone && ready };
295 }
296
297 export function GeneratingBody({ cats, rows, pct, finalizing, done = false, contentType = "docs" }) {
298 const rowFor = (c) => rows.find((r) => r.id === c.id) || { state: "pending", written: 0 };
299 // Content-type-aware progress copy: a knowledge base, an FAQ list, and product
300 // FAQs are different things, so the label must match what's being generated.
301 // `ready` is the terminal 100% message shown once generation is complete — it
302 // must not fall back to "Building…" in the moment before we switch screens.
303 const COPY = {
304 docs: { building: __("Building your knowledge base…", "betterdocs"), finalizing: __("Finalizing your knowledge base…", "betterdocs"), ready: __("Knowledge base is ready", "betterdocs") },
305 faq: { building: __("Building your FAQs…", "betterdocs"), finalizing: __("Finalizing your FAQs…", "betterdocs"), ready: __("FAQs are ready", "betterdocs") },
306 product_faq: { building: __("Building your product FAQs…", "betterdocs"), finalizing: __("Finalizing your product FAQs…", "betterdocs"), ready: __("Product FAQs are ready", "betterdocs") },
307 };
308 const copy = COPY[contentType] || COPY.docs;
309 return (
310 <div className="betterdocs-sd-fade">
311 <div className="betterdocs-sd-progress-head">
312 <span className="betterdocs-sd-progress-label">
313 {done ? copy.ready : (finalizing || pct === 100) ? copy.finalizing : copy.building}
314 </span>
315 <span className="betterdocs-sd-progress-pct">{pct}%</span>
316 </div>
317 <div className="betterdocs-sd-progress-track">
318 <div className="betterdocs-sd-progress-fill" style={{ width: `${pct}%` }} />
319 </div>
320
321 <div className="betterdocs-sd-progress-rows">
322 {cats.map((c) => {
323 const r = rowFor(c);
324 const done = r.state === "done";
325 const live = r.state === "creating" || r.state === "writing";
326 // A FAQ topic has no planned question count (the AI decides), so its
327 // rows can't show k/N — they'd read "Writing 1/0 questions…".
328 const planned = c.articles.length;
329 const sub =
330 r.state === "creating"
331 ? sprintf(__("Creating %s…", "betterdocs"), decodeEntities(c.name))
332 : r.state === "writing"
333 ? (!planned
334 ? __("Writing questions…", "betterdocs")
335 : contentType === "docs"
336 ? sprintf(__("Writing %1$d/%2$d docs…", "betterdocs"), r.written, planned)
337 : sprintf(__("Writing %1$d/%2$d questions…", "betterdocs"), r.written, planned))
338 : done
339 ? (!planned
340 ? __("Questions ready", "betterdocs")
341 : contentType === "docs"
342 ? sprintf(_n("%d doc ready", "%d docs ready", planned, "betterdocs"), planned)
343 : sprintf(_n("%d question ready", "%d questions ready", planned, "betterdocs"), planned))
344 : __("Queued", "betterdocs");
345 return (
346 <div
347 key={c.id}
348 className={`betterdocs-sd-progress-row${live ? " is-live" : ""}${r.state === "pending" ? " is-pending" : ""}`}
349 >
350 <div className="betterdocs-sd-progress-row-meta">
351 <div className="betterdocs-sd-progress-row-name">{decodeEntities(c.name)}</div>
352 <div className={`betterdocs-sd-progress-row-sub${done ? " is-done" : ""}`}>{sub}</div>
353 </div>
354 <div className="betterdocs-sd-progress-row-state">
355 {done ? (
356 <span className="betterdocs-sd-progress-check"><IconCheck size={14} sw={3} /></span>
357 ) : live ? (
358 <Spinner size={20} />
359 ) : (
360 <span className="betterdocs-sd-progress-dot" />
361 )}
362 </div>
363 </div>
364 );
365 })}
366 </div>
367
368 {(finalizing || pct === 100) && (
369 <div className={`betterdocs-sd-progress-finalizing${done ? " is-done" : ""}`}>
370 <span className="betterdocs-sd-progress-finalizing-icon">
371 {done ? <IconCheck size={14} sw={3} /> : <Spinner size={18} />}
372 </span>
373 <span className="betterdocs-sd-progress-finalizing-text">
374 {done ? copy.ready : copy.finalizing}
375 </span>
376 </div>
377 )}
378 </div>
379 );
380 }
381
382 /* ---------- Preview groups ---------- */
383 export function PreviewGroups({ cats, contentType = "docs" }) {
384 return (
385 <div className="betterdocs-sd-preview">
386 {cats.map((c) => (
387 <div key={c.id} className="betterdocs-sd-preview-group betterdocs-sd-fade">
388 <div className="betterdocs-sd-preview-head">
389 <span className="betterdocs-sd-preview-name">{decodeEntities(c.name)}</span>
390 {/* The consolidated store-wide group shows on every product page; label it
391 so the owner knows it isn't tied to one product category. */}
392 {c.all_products && (
393 <span className="betterdocs-sd-preview-tag" title={__("Shown on every product page", "betterdocs")}>
394 {__("All products", "betterdocs")}
395 </span>
396 )}
397 <span className="betterdocs-sd-preview-badge">
398 {itemCount(c.articles.length, contentType)}
399 </span>
400 </div>
401 <div>
402 {c.articles.map((a, i) => (
403 <div key={i} className="betterdocs-sd-preview-article">
404 <span className="betterdocs-sd-preview-article-icon"><IconDoc size={16} /></span>
405 <span className="betterdocs-sd-preview-article-title">{articleTitle(a)}</span>
406 <span className="betterdocs-sd-preview-article-draft">{__("Draft", "betterdocs")}</span>
407 </div>
408 ))}
409 </div>
410 </div>
411 ))}
412 </div>
413 );
414 }
415
416 /* ---------- Populated docs — All Docs grid look (read-only) ----------
417 Mirrors the dashboard category grid (betterdocs-row > col-md-4 >
418 category-wrap). Read-only: drag handle is shown but there are no hover
419 action buttons and no "Add New Doc" footer; height follows content. */
420 export function PopulatedDocs({ cats }) {
421 return (
422 <div className="betterdocs-sd-populated">
423 <div className="betterdocs-sd-populated-header">
424 <div className="betterdocs-sd-populated-header-left">
425 <h2 className="betterdocs-sd-populated-title">{__("All Docs", "betterdocs")}</h2>
426 <span className="betterdocs-sd-populated-badge">{__("AI-generated", "betterdocs")}</span>
427 </div>
428 </div>
429 <div className="betterdocs-sd-populated-grid betterdocs-row">
430 {cats.map((c) => (
431 <div key={c.id} className="betterdocs-col betterdocs-col-md-4 betterdocs-sd-fade">
432 <div className="betterdocs-sd-cat-wrap">
433 <div className="betterdocs-sd-cat-header">
434 <h3 className="betterdocs-sd-cat-name">{decodeEntities(c.name)}</h3>
435 </div>
436 <ul className="betterdocs-sd-cat-list">
437 {c.articles.map((a, i) => (
438 <li key={i}>
439 <span className="betterdocs-sd-doc-drag">
440 <IconDrag />
441 </span>
442 <span className="betterdocs-sd-doc-title">{articleTitle(a)}</span>
443 </li>
444 ))}
445 </ul>
446 </div>
447 </div>
448 ))}
449 </div>
450 </div>
451 );
452 }
453
454 /* ---------- helpers ---------- */
455 export function articleTitle(a) {
456 return decodeEntities(typeof a === "string" ? a : a?.title || "");
457 }
458
459 /* Count label matched to what's being generated and correctly singular/plural:
460 docs → "1 doc" / "3 docs"; FAQ & product FAQ → "1 question" / "3 questions". */
461 export function itemCount(n, contentType = "docs") {
462 return contentType === "docs"
463 ? sprintf(_n("%d doc", "%d docs", n, "betterdocs"), n)
464 : sprintf(_n("%d question", "%d questions", n, "betterdocs"), n);
465 }
466
467 function humanType(type) {
468 const map = {
469 ecommerce: __("WooCommerce store", "betterdocs"),
470 course_lms: __("course / LMS site", "betterdocs"),
471 digital_downloads: __("digital downloads store", "betterdocs"),
472 membership: __("membership site", "betterdocs"),
473 community: __("community site", "betterdocs"),
474 business_site: __("business site", "betterdocs"),
475 general: __("website", "betterdocs"),
476 };
477 return map[type] || map.general;
478 }
479
480 /* Prefix the right indefinite article ("a"/"an") for a human label, so e.g.
481 "AI / LLM platform" reads "an AI / LLM platform" and "WooCommerce store"
482 reads "a WooCommerce store". */
483 function withArticle(label) {
484 const l = String(label || "").trim();
485 if (!l) return __("a website", "betterdocs");
486 return /^[aeiou]/i.test(l) ? `an ${l}` : `a ${l}`;
487 }
488