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 / settings / fields / EditAIActions.js

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

339 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useMemo, useState } from "react";
2 import { __ } from "@wordpress/i18n";
3 import { v4 } from "uuid";
4 import { useBuilderContext, withLabel } from "quickbuilder";
5 import ReactModal from "react-modal";
6 import Swal from "sweetalert2";
7 import CloseIcon from "../components/CloseIcon";
8 import { apiFetch, toastAlert } from "../../utils";
9
10 // ── Edit AI Personalize → Edit with AI actions ───────────────────────────────
11 // List + modal editor, mirroring the Write AI Personalize instruction sets.
12 // Predefined actions (predefined === true) can be toggled on/off and have their
13 // instruction edited, but their label is locked and they can't be deleted. Custom
14 // actions can be added, fully edited, and removed. Enable toggles persist inline;
15 // add/edit happens in a popup, persisted via the settings endpoint (merges per-key).
16
17 // Predefined actions whose prompt carries a {option} placeholder, with the label
18 // of the option BetterDocs substitutes at runtime (tone / language picker).
19 const OPTION_HINTS = {
20 change_tone: __("tone", "betterdocs"),
21 translate: __("language", "betterdocs"),
22 };
23
24 // Row action icons (match WwaInstructions.js / BetterRepeaterValueShow.js).
25 const EditIcon = ({ onClick }) => (
26 <svg xmlns="http://www.w3.org/2000/svg" width={12} height={12} viewBox="0 0 12 12" fill="none" onClick={onClick}>
27 <path fillRule="evenodd" clipRule="evenodd" d="M8.60581 0.582347C8.97868 0.209473 9.48441 0 10.0117 0C10.2729 0 10.5314 0.0514267 10.7726 0.151347C11.0139 0.251267 11.233 0.39772 11.4177 0.582347C11.6023 0.766973 11.7487 0.98616 11.8487 1.22739C11.9486 1.46861 12 1.72716 12 1.98826C12 2.24936 11.9486 2.50791 11.8487 2.74913C11.7487 2.99036 11.6023 3.20955 11.4177 3.39417L10.6821 4.12977C10.4217 4.39013 9.99961 4.39013 9.73928 4.12978L7.87021 2.26076C7.60988 2.00041 7.60988 1.5783 7.87021 1.31795L8.60581 0.582347ZM6.92741 3.20357C6.66708 2.94322 6.24495 2.94322 5.98461 3.20357L1.34057 7.8476C0.913375 8.2748 0.610308 8.81007 0.463781 9.3962L0.0199214 11.1717C-0.0368786 11.3988 0.0296881 11.6391 0.195275 11.8047C0.360861 11.9703 0.601188 12.0369 0.828375 11.9801L2.60382 11.5362C3.18993 11.3897 3.72519 11.0867 4.15239 10.6595L8.79641 6.0154C9.05681 5.75507 9.05681 5.33293 8.79641 5.0726L6.92741 3.20357Z" fill="#718096" />
28 <path fillRule="evenodd" clipRule="evenodd" d="M6 11.3346C6 10.9664 6.29847 10.668 6.66667 10.668H11.3333C11.7015 10.668 12 10.9664 12 11.3346C12 11.7028 11.7015 12.0013 11.3333 12.0013H6.66667C6.29847 12.0013 6 11.7028 6 11.3346Z" fill="#718096" />
29 </svg>
30 );
31
32 const DeleteIcon = ({ onClick }) => (
33 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 16 16" fill="none" onClick={onClick}>
34 <path d="M2 3.9987H3.33333M3.33333 3.9987H14M3.33333 3.9987V13.332C3.33333 13.6857 3.47381 14.0248 3.72386 14.2748C3.97391 14.5249 4.31304 14.6654 4.66667 14.6654H11.3333C11.687 14.6654 12.0261 14.5249 12.2761 14.2748C12.5262 14.0248 12.6667 13.6857 12.6667 13.332V3.9987H3.33333ZM5.33333 3.9987V2.66536C5.33333 2.31174 5.47381 1.9726 5.72386 1.72256C5.97391 1.47251 6.31304 1.33203 6.66667 1.33203H9.33333C9.68696 1.33203 10.0261 1.47251 10.2761 1.72256C10.5262 1.9726 10.6667 2.31174 10.6667 2.66536V3.9987M6.66667 7.33203V11.332M9.33333 7.33203V11.332" stroke="#98A2B3" strokeWidth={1.66667} strokeLinecap="round" strokeLinejoin="round" />
35 </svg>
36 );
37
38 // Lightweight enable/disable switch — self-contained inline styles so the field
39 // doesn't depend on quickbuilder's toggle markup.
40 const Toggle = ({ checked, onChange }) => (
41 <button
42 type="button"
43 role="switch"
44 aria-checked={checked}
45 onClick={() => onChange(!checked)}
46 style={{
47 position: "relative", width: 36, height: 20, borderRadius: 999,
48 border: "none", cursor: "pointer", padding: 0, flex: "0 0 auto",
49 background: checked ? "#00b884" : "#cbd2d9", transition: "background .15s",
50 }}
51 >
52 <span style={{
53 position: "absolute", top: 2, left: checked ? 18 : 2, width: 16, height: 16,
54 borderRadius: "50%", background: "#fff", transition: "left .15s",
55 boxShadow: "0 1px 2px rgba(0,0,0,0.2)",
56 }} />
57 </button>
58 );
59
60 const formStyles = {
61 label: { display: "block", fontSize: 13, fontWeight: 600, color: "#1e2329", margin: "0 0 6px" },
62 input: {
63 width: "100%", boxSizing: "border-box", border: "1px solid #d8dde2", borderRadius: 7,
64 padding: "10px 12px", fontSize: 14, color: "#1e2329", outline: "none",
65 },
66 textarea: {
67 width: "100%", boxSizing: "border-box", minHeight: 180, resize: "vertical",
68 border: "1px solid #d8dde2", borderRadius: 8, padding: "11px 13px", fontSize: 13, lineHeight: 1.55,
69 fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
70 color: "#1e2329", outline: "none",
71 },
72 group: { marginBottom: 18 },
73 lockedNote: { fontSize: 12.5, color: "#667085", marginTop: 6 },
74 footer: {
75 display: "flex", justifyContent: "flex-end", alignItems: "center",
76 gap: 10, marginTop: 26,
77 },
78 btnCancel: {
79 background: "#fff", border: "1px solid #d0d5dd", color: "#344054",
80 borderRadius: 8, padding: "10px 20px", fontSize: 14, fontWeight: 600,
81 cursor: "pointer", lineHeight: 1.42,
82 },
83 btnSave: {
84 background: "#00b884", border: "none", color: "#fff",
85 borderRadius: 8, padding: "10px 24px", fontSize: 14, fontWeight: 600,
86 cursor: "pointer", lineHeight: 1.42,
87 },
88 };
89
90 // react-modal overlay + content for the shared FAQ-builder modal shell
91 // (.bd-faq-ctg-modal), matching CategoryModal so the modal reads identically.
92 const customStyles = {
93 overlay: {
94 position: "fixed", top: 0, left: 0, right: 0, bottom: 0,
95 backgroundColor: "rgba(35, 40, 45, 0.5)", zIndex: 99999,
96 padding: 50, paddingTop: 70, display: "flex", flexDirection: "column", overflow: "auto",
97 },
98 content: {
99 width: "40rem", maxWidth: "90%", position: "static", height: "auto", overflow: "initial", margin: "auto",
100 },
101 };
102
103 const noteStyle = { margin: "6px 0 0", fontSize: 12.5, lineHeight: 1.45, color: "var(--text-color-600, #667085)" };
104
105 const EditAIActions = (props) => {
106 const builderContext = useBuilderContext();
107 const fieldName = props.name;
108
109 const raw = builderContext.values?.[fieldName];
110 const items = useMemo(() => {
111 const source = Array.isArray(raw) ? raw : Array.isArray(props.value) ? props.value : [];
112 return source.map((it) => ({
113 id: it?.id || v4(),
114 label: it?.label ?? "",
115 instruction: it?.instruction ?? "",
116 enabled: it?.enabled === undefined ? true : !!it.enabled,
117 predefined: !!it?.predefined,
118 }));
119 }, [raw, props.value]);
120
121 // editing: { index, isNew, draft } | null
122 const [editing, setEditing] = useState(null);
123
124 // Update form state for immediate UI feedback, then persist this single field
125 // via the settings endpoint (it merges per-key) — no page-level Save needed.
126 const persist = (next) => {
127 builderContext.setFieldValue(fieldName, next);
128 apiFetch
129 .post("settings", { [fieldName]: next, _active_tab: "tab-betterdocs-ai" })
130 .then((res) => {
131 if (res?.success) {
132 toastAlert("success", __("Actions saved.", "betterdocs"));
133 } else {
134 toastAlert(
135 res?.code === "nothing_changed" ? "info" : "error",
136 __(res?.message ?? "Couldn't save actions.", "betterdocs")
137 );
138 }
139 });
140 };
141
142 const toast = (type, msg) => {
143 if (props?.context?.alerts?.toast) props.context.alerts.toast(type, msg);
144 else toastAlert(type, msg);
145 };
146
147 const onToggle = (index, enabled) => {
148 persist(items.map((it, i) => (i === index ? { ...it, enabled } : it)));
149 };
150
151 const openEdit = (index) => {
152 setEditing({ index, isNew: false, draft: { ...items[index] } });
153 };
154
155 const openAdd = () => {
156 setEditing({
157 index: -1,
158 isNew: true,
159 draft: { id: v4(), label: "", instruction: "", enabled: true, predefined: false },
160 });
161 };
162
163 const closeModal = () => setEditing(null);
164
165 const saveModal = () => {
166 if (!editing) return;
167 const draft = {
168 id: editing.draft.id || v4(),
169 label: (editing.draft.label || "").trim(),
170 instruction: editing.draft.instruction || "",
171 enabled: editing.draft.enabled === undefined ? true : !!editing.draft.enabled,
172 predefined: !!editing.draft.predefined,
173 };
174 if (!draft.predefined && draft.label === "") {
175 toast("error", __("Please add a name for this action.", "betterdocs"));
176 return;
177 }
178 if (draft.instruction.trim() === "") {
179 toast("error", __("Instruction can't be empty.", "betterdocs"));
180 return;
181 }
182
183 if (editing.isNew) {
184 persist([...items, draft]);
185 } else {
186 persist(items.map((it, i) => (i === editing.index ? draft : it)));
187 }
188 setEditing(null);
189 };
190
191 const onDelete = (index) => {
192 if (items[index]?.predefined) return;
193 Swal.fire({
194 text: __("Are you sure you want to delete this action?", "betterdocs"),
195 icon: "warning",
196 showConfirmButton: true,
197 showCancelButton: true,
198 confirmButtonText: __("Yes", "betterdocs"),
199 cancelButtonText: __("No", "betterdocs"),
200 customClass: { popup: "betterdocs-bulk-action-popup" },
201 }).then((result) => {
202 if (result?.isConfirmed) {
203 persist(items.filter((_, i) => i !== index));
204 }
205 });
206 };
207
208 const setDraft = (patch) =>
209 setEditing((e) => (e ? { ...e, draft: { ...e.draft, ...patch } } : e));
210
211 const editingIsPredefined = editing && editing.draft.predefined;
212 const editingOptionHint = editing && OPTION_HINTS[editing.draft.id];
213
214 return (
215 <div className="wprf-control-field wwa-instructions-field edit-ai-actions-field">
216 <div className="wprf-repeater-control">
217 <div className="wprf-repeater-content">
218 {items.map((item, index) => (
219 <div className="wprf-repeater-field" key={item.id}>
220 <div className="wprf-repeater-inner-field betterdocs">
221 <div className="wprf-repeater-inner-field-item">
222 <p>{item.label || __("(Untitled)", "betterdocs")}</p>
223 </div>
224 <div className="betterdocs-action-toggle-wrapper">
225 <div className="betterdocs-cookies-list-action">
226 <ul id="betterdocs-admin-actions-ul" style={{ display: "flex", alignItems: "center", gap: 12 }}>
227 <Toggle checked={item.enabled} onChange={(v) => onToggle(index, v)} />
228 <EditIcon onClick={() => openEdit(index)} />
229 {!item.predefined && (
230 <DeleteIcon onClick={() => onDelete(index)} />
231 )}
232 </ul>
233 </div>
234 </div>
235 </div>
236 </div>
237 ))}
238 </div>
239
240 <div className="wprf-repeater-label" onClick={openAdd}>
241 <button type="button" className="wprf-repeater-button" onClick={openAdd}>
242 + {__("Add Action", "betterdocs")}
243 </button>
244 </div>
245 </div>
246
247 <ReactModal
248 isOpen={!!editing}
249 ariaHideApp={false}
250 className="bd-faq-ctg-modal"
251 onRequestClose={closeModal}
252 style={customStyles}
253 >
254 <div className="bd-faq-ctg-header">
255 <span
256 className="bd-faq-stat-card__icon"
257 style={{ "--bd-icon-tint": "#7f56d9", "--bd-icon-tint-rgb": "127, 86, 217" }}
258 >
259 <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round">
260 <path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z" />
261 <path d="M19 15l.9 2.4L22 18l-2.1.6L19 21l-.9-2.4L16 18l2.1-.6z" />
262 </svg>
263 </span>
264 <div className="bd-faq-ctg-heading">
265 <h3 className="bd-faq-ctg-title">
266 {editing && editing.isNew ? __("Add Action", "betterdocs") : __("Edit Action", "betterdocs")}
267 </h3>
268 <p className="bd-faq-ctg-subtitle">
269 {__("Name the action and write the instruction BetterDocs AI should follow when it runs on the selected content.", "betterdocs")}
270 </p>
271 </div>
272 </div>
273
274 {editing && (
275 <div className="bd-faq-ctg-body">
276 <div className="bd-faq-cgt-field">
277 <label className="bd-faq-label">{__("Action name", "betterdocs")}</label>
278 <input
279 type="text"
280 className="bd-faq-input"
281 style={editingIsPredefined ? { background: "var(--text-color-100)", color: "var(--text-color-600)", cursor: "not-allowed" } : undefined}
282 value={editing.draft.label}
283 readOnly={editingIsPredefined}
284 placeholder={__("e.g. Add a TL;DR", "betterdocs")}
285 onChange={(e) => setDraft({ label: e.target.value })}
286 />
287 {editingIsPredefined && (
288 <p style={noteStyle}>
289 {__("This is a built-in action — its name is locked, but you can edit its instruction or turn it off.", "betterdocs")}
290 </p>
291 )}
292 </div>
293
294 <div className="bd-faq-cgt-field">
295 <label className="bd-faq-label">{__("Instruction", "betterdocs")}</label>
296 <textarea
297 className="bd-faq-textarea"
298 rows={5}
299 value={editing.draft.instruction}
300 placeholder={__("Describe exactly what the AI should do with the selected content…", "betterdocs")}
301 onChange={(e) => setDraft({ instruction: e.target.value })}
302 />
303 {editingOptionHint && (
304 <p style={noteStyle}>{sprintfOption(editingOptionHint)}</p>
305 )}
306 </div>
307 </div>
308 )}
309
310 <div className="bd-faq-ctg-footer">
311 <button type="button" className="bd-faq-button button-ghost" onClick={closeModal}>
312 <span className="bd-faq-text">{__("Cancel", "betterdocs")}</span>
313 </button>
314 <button type="button" className="bd-faq-button button-success" onClick={saveModal}>
315 <span className="bd-faq-text">{editing && editing.isNew ? __("Save", "betterdocs") : __("Update", "betterdocs")}</span>
316 </button>
317 </div>
318
319 <button type="button" className="bd-faq-modal-close-button" onClick={closeModal}>
320 <svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
321 <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" />
322 </svg>
323 </button>
324 </ReactModal>
325 </div>
326 );
327 };
328
329 // Keep the {option} placeholder explanation readable without string concatenation
330 // inside JSX. `which` is "tone" or "language".
331 const sprintfOption = (which) =>
332 __("Keep the ", "betterdocs") +
333 "{option}" +
334 __(" placeholder where the selected ", "betterdocs") +
335 which +
336 __(" should appear.", "betterdocs");
337
338 export default withLabel(React.memo(EditAIActions));
339