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 / WwaInstructions.js

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

344 lines 18.0 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 // ── Personalize → Write with AI instruction sets ─────────────────────────────
11 // List + modal editor, mirroring the Access & Restrictions "rules" repeater.
12 // The "Default/Core" set (id === 'default') is always applied as the base system
13 // prompt: it can be edited but never deleted, and its title is locked. Other sets
14 // can be added, edited, and removed. Editing happens in a popup (title +
15 // instructions + .md upload), persisted with the page's main Save button.
16 const DEFAULT_ID = "default";
17
18 // Row action icons (match BetterRepeaterValueShow.js).
19 const EditIcon = ({ onClick }) => (
20 <svg xmlns="http://www.w3.org/2000/svg" width={12} height={12} viewBox="0 0 12 12" fill="none" onClick={onClick}>
21 <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" />
22 <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" />
23 </svg>
24 );
25
26 const DeleteIcon = ({ onClick }) => (
27 <svg xmlns="http://www.w3.org/2000/svg" width={16} height={16} viewBox="0 0 16 16" fill="none" onClick={onClick}>
28 <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" />
29 </svg>
30 );
31
32 const formStyles = {
33 label: { display: "block", fontSize: 13, fontWeight: 600, color: "#1e2329", margin: "0 0 6px" },
34 input: {
35 width: "100%", boxSizing: "border-box", border: "1px solid #d8dde2", borderRadius: 7,
36 padding: "10px 12px", fontSize: 14, color: "#1e2329", outline: "none",
37 },
38 textarea: {
39 width: "100%", boxSizing: "border-box", minHeight: 220, resize: "vertical",
40 border: "1px solid #d8dde2", borderRadius: 8, padding: "11px 13px", fontSize: 13, lineHeight: 1.55,
41 fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
42 color: "#1e2329", outline: "none",
43 },
44 upload: {
45 display: "inline-flex", alignItems: "center", gap: 7, fontSize: 13, fontWeight: 600,
46 color: "#475467", background: "#f4f6f8", border: "1px solid #d8dde2", borderRadius: 7,
47 padding: "8px 13px", cursor: "pointer",
48 },
49 group: { marginBottom: 18 },
50 lockedNote: { fontSize: 12.5, color: "#667085", marginTop: 6 },
51 footer: {
52 display: "flex", justifyContent: "flex-end", alignItems: "center",
53 gap: 10, marginTop: 26,
54 },
55 btnCancel: {
56 background: "#fff", border: "1px solid #d0d5dd", color: "#344054",
57 borderRadius: 8, padding: "10px 20px", fontSize: 14, fontWeight: 600,
58 cursor: "pointer", lineHeight: 1.42,
59 },
60 btnSave: {
61 background: "#00b884", border: "none", color: "#fff",
62 borderRadius: 8, padding: "10px 24px", fontSize: 14, fontWeight: 600,
63 cursor: "pointer", lineHeight: 1.42,
64 },
65 };
66
67 // react-modal overlay + content for the shared FAQ-builder modal shell
68 // (.bd-faq-ctg-modal), matching CategoryModal so the modal reads identically.
69 const customStyles = {
70 overlay: {
71 position: "fixed", top: 0, left: 0, right: 0, bottom: 0,
72 backgroundColor: "rgba(35, 40, 45, 0.5)", zIndex: 99999,
73 padding: 50, paddingTop: 70, display: "flex", flexDirection: "column", overflow: "auto",
74 },
75 content: {
76 width: "40rem", maxWidth: "90%", position: "static", height: "auto", overflow: "initial", margin: "auto",
77 },
78 };
79
80 const noteStyle = { margin: "6px 0 0", fontSize: 12.5, lineHeight: 1.45, color: "var(--text-color-600, #667085)" };
81
82 const WwaInstructions = (props) => {
83 const builderContext = useBuilderContext();
84 const fieldName = props.name;
85
86 const raw = builderContext.values?.[fieldName];
87 const items = useMemo(() => {
88 const source = Array.isArray(raw) ? raw : Array.isArray(props.value) ? props.value : [];
89 return source.map((it) => ({
90 id: it?.id || v4(),
91 title: it?.title ?? "",
92 content: it?.content ?? "",
93 }));
94 }, [raw, props.value]);
95
96 // editing: { index, isNew, draft } | null
97 const [editing, setEditing] = useState(null);
98
99 // Update the form state for immediate UI feedback, then persist this single
100 // field via the existing settings endpoint (it merges per-key). This lets the
101 // Personalize tab self-save from the modal — no page-level Save button needed.
102 const persist = (next) => {
103 builderContext.setFieldValue(fieldName, next);
104 apiFetch
105 .post("settings", { [fieldName]: next, _active_tab: "tab-betterdocs-ai" })
106 .then((res) => {
107 if (res?.success) {
108 toastAlert("success", __("Instructions saved.", "betterdocs"));
109 } else {
110 toastAlert(
111 res?.code === "nothing_changed" ? "info" : "error",
112 __(res?.message ?? "Couldn't save instructions.", "betterdocs")
113 );
114 }
115 });
116 };
117
118 const toast = (type, msg) => {
119 if (props?.context?.alerts?.toast) props.context.alerts.toast(type, msg);
120 else toastAlert(type, msg);
121 };
122
123 const openEdit = (index) => {
124 setEditing({ index, isNew: false, draft: { ...items[index] } });
125 };
126
127 const openAdd = () => {
128 setEditing({ index: -1, isNew: true, draft: { id: v4(), title: "", content: "" } });
129 };
130
131 const closeModal = () => setEditing(null);
132
133 const saveModal = () => {
134 if (!editing) return;
135 const draft = {
136 id: editing.draft.id || v4(),
137 title: (editing.draft.title || "").trim(),
138 content: editing.draft.content || "",
139 };
140 const isDefault = draft.id === DEFAULT_ID;
141 if (!isDefault && draft.title === "") {
142 toast("error", __("Please add a title for this instruction.", "betterdocs"));
143 return;
144 }
145 if (draft.content.trim() === "") {
146 toast("error", __("Instructions can't be empty.", "betterdocs"));
147 return;
148 }
149
150 if (editing.isNew) {
151 persist([...items, draft]);
152 } else {
153 persist(items.map((it, i) => (i === editing.index ? draft : it)));
154 }
155 setEditing(null);
156 };
157
158 const onDelete = (index) => {
159 if (items[index]?.id === DEFAULT_ID) return;
160 Swal.fire({
161 text: __("Are you sure you want to delete this instruction?", "betterdocs"),
162 icon: "warning",
163 showConfirmButton: true,
164 showCancelButton: true,
165 confirmButtonText: __("Yes", "betterdocs"),
166 cancelButtonText: __("No", "betterdocs"),
167 customClass: { popup: "betterdocs-bulk-action-popup" },
168 }).then((result) => {
169 if (result?.isConfirmed) {
170 persist(items.filter((_, i) => i !== index));
171 }
172 });
173 };
174
175 const setDraft = (patch) =>
176 setEditing((e) => (e ? { ...e, draft: { ...e.draft, ...patch } } : e));
177
178 const handleUpload = (e) => {
179 const file = e.target.files && e.target.files[0];
180 e.target.value = "";
181 if (!file) return;
182 const name = (file.name || "").toLowerCase();
183 const isMd =
184 name.endsWith(".md") || name.endsWith(".markdown") ||
185 file.type === "text/markdown" || file.type === "text/plain" || file.type === "text/x-markdown";
186 if (!isMd) {
187 toast("error", __("Please upload a .md (Markdown) file.", "betterdocs"));
188 return;
189 }
190 if (file.size === 0) {
191 toast("error", __("File can't be empty.", "betterdocs"));
192 return;
193 }
194 const reader = new FileReader();
195 reader.onload = (event) => setDraft({ content: String(event?.target?.result || "") });
196 reader.readAsText(file);
197 };
198
199 const editingIsDefault = editing && editing.draft.id === DEFAULT_ID;
200
201 return (
202 <div className="wprf-control-field wwa-instructions-field">
203 <div className="wprf-repeater-control">
204 <div className="wprf-repeater-content">
205 {items.map((item, index) => {
206 const isDefault = item.id === DEFAULT_ID;
207 return (
208 <div className="wprf-repeater-field" key={item.id}>
209 <div className="wprf-repeater-inner-field betterdocs">
210 <div className="wprf-repeater-inner-field-item">
211 <p>{item.title || __("(Untitled)", "betterdocs")}</p>
212 </div>
213 <div className="betterdocs-action-toggle-wrapper">
214 <div className="betterdocs-cookies-list-action">
215 <ul id="betterdocs-admin-actions-ul">
216 <EditIcon onClick={() => openEdit(index)} />
217 {!isDefault && (
218 <DeleteIcon onClick={() => onDelete(index)} />
219 )}
220 </ul>
221 </div>
222 </div>
223 </div>
224 </div>
225 );
226 })}
227 </div>
228
229 <div className="wprf-repeater-label" onClick={openAdd}>
230 <button type="button" className="wprf-repeater-button" onClick={openAdd}>
231 + {__("Add Instruction", "betterdocs")}
232 </button>
233 </div>
234 </div>
235
236 <ReactModal
237 isOpen={!!editing}
238 ariaHideApp={false}
239 className="bd-faq-ctg-modal"
240 onRequestClose={closeModal}
241 style={customStyles}
242 >
243 <div className="bd-faq-ctg-header">
244 <span
245 className="bd-faq-stat-card__icon"
246 style={{ "--bd-icon-tint": "#7f56d9", "--bd-icon-tint-rgb": "127, 86, 217" }}
247 >
248 <svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round">
249 <path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z" />
250 <path d="M19 15l.9 2.4L22 18l-2.1.6L19 21l-.9-2.4L16 18l2.1-.6z" />
251 </svg>
252 </span>
253 <div className="bd-faq-ctg-heading">
254 <h3 className="bd-faq-ctg-title">
255 {editing && editing.isNew ? __("Add Instruction", "betterdocs") : __("Edit Instruction", "betterdocs")}
256 </h3>
257 <p className="bd-faq-ctg-subtitle">
258 {__("Give this instruction set a title and write (or upload) the instructions BetterDocs AI should follow.", "betterdocs")}
259 </p>
260 </div>
261 </div>
262
263 {editing && (
264 <div className="bd-faq-ctg-body">
265 <div className="bd-faq-cgt-field">
266 <label className="bd-faq-label">{__("Title", "betterdocs")}</label>
267 <input
268 type="text"
269 className="bd-faq-input"
270 style={editingIsDefault ? { background: "var(--text-color-100)", color: "var(--text-color-600)", cursor: "not-allowed" } : undefined}
271 value={editing.draft.title}
272 readOnly={editingIsDefault}
273 placeholder={__("e.g. API Reference voice", "betterdocs")}
274 onChange={(e) => setDraft({ title: e.target.value })}
275 />
276 {editingIsDefault && (
277 <p style={noteStyle}>
278 {__("The Default/Core set is always applied as the base prompt and cannot be renamed or deleted.", "betterdocs")}
279 </p>
280 )}
281 </div>
282
283 <div className="bd-faq-cgt-field">
284 <label className="bd-faq-label">{__("Instructions", "betterdocs")}</label>
285 <textarea
286 className="bd-faq-textarea"
287 rows={6}
288 value={editing.draft.content}
289 placeholder={__("Write instructions, or paste Markdown here…", "betterdocs")}
290 onChange={(e) => setDraft({ content: e.target.value })}
291 />
292 <label
293 style={{
294 display: "inline-flex", alignItems: "center", gap: 8,
295 alignSelf: "flex-start", marginTop: 10, cursor: "pointer",
296 height: 38, padding: "0 15px",
297 border: "1px dashed var(--text-color-300, #dde2e4)", borderRadius: 8,
298 background: "var(--base-background, #fff)",
299 fontSize: 13, fontWeight: 600, color: "var(--text-color-700, #344054)",
300 transition: "border-color .15s, color .15s, background .15s",
301 }}
302 onMouseEnter={(e) => { e.currentTarget.style.borderColor = "var(--base-color-700, #00b884)"; e.currentTarget.style.background = "var(--base-color-75, #ecfdf3)"; }}
303 onMouseLeave={(e) => { e.currentTarget.style.borderColor = "var(--text-color-300, #dde2e4)"; e.currentTarget.style.background = "var(--base-background, #fff)"; }}
304 >
305 <span style={{ display: "flex", color: "var(--base-color-700, #00b884)" }}>
306 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
307 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
308 <path d="M17 8l-5-5-5 5" />
309 <path d="M12 3v12" />
310 </svg>
311 </span>
312 {__("Upload a .md file", "betterdocs")}
313 <input
314 type="file"
315 accept=".md,.markdown,text/markdown,text/plain"
316 style={{ display: "none" }}
317 onChange={handleUpload}
318 />
319 </label>
320 </div>
321 </div>
322 )}
323
324 <div className="bd-faq-ctg-footer">
325 <button type="button" className="bd-faq-button button-ghost" onClick={closeModal}>
326 <span className="bd-faq-text">{__("Cancel", "betterdocs")}</span>
327 </button>
328 <button type="button" className="bd-faq-button button-success" onClick={saveModal}>
329 <span className="bd-faq-text">{editing && editing.isNew ? __("Save", "betterdocs") : __("Update", "betterdocs")}</span>
330 </button>
331 </div>
332
333 <button type="button" className="bd-faq-modal-close-button" onClick={closeModal}>
334 <svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
335 <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" />
336 </svg>
337 </button>
338 </ReactModal>
339 </div>
340 );
341 };
342
343 export default withLabel(React.memo(WwaInstructions));
344