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

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

257 lines 10.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ===== ai-shared/InstructionPicker.js =====
2 // Shared selector for saved "Write with AI" instruction sets, used by both the
3 // Write Documentation modal and the AI Edit modal. The "Default" set is never
4 // listed here — the backend always applies it as the base system prompt; this
5 // picker only selects the *extra* sets layered on top.
6 //
7 // Rendering rules (per product spec):
8 // • 0 sets → render nothing (caller usually guards too).
9 // • 1–4 set → segmented toggle buttons (multi-select).
10 // • ≥ 5 set → multi-select dropdown with checkboxes.
11
12 import { __, sprintf } from "@wordpress/i18n";
13 import { useState, useRef, useEffect } from "@wordpress/element";
14 import { IcChevDown, IcCheck, AIcon } from "./kit";
15
16 const BUTTON_LIMIT = 4;
17
18 // Default section wording — shared verbatim by the Write-with-AI and Edit-with-AI
19 // modals so the "Instructions" picker looks and reads the same in both places.
20 const DEFAULT_LABEL = __("Instructions", "betterdocs");
21 const DEFAULT_HINT = __("Optional — saved instructions that steer the AI", "betterdocs");
22
23 // Small compass glyph shown before each chip title — "guides" that steer the AI.
24 const IcGuide = (p) => (
25 <AIcon sw={1.7} {...p}>
26 <circle cx="12" cy="12" r="9" />
27 <path d="M15.5 8.5l-2 5-5 2 2-5z" />
28 </AIcon>
29 );
30
31 // Capitalized section label + right-aligned muted hint — mirrors the FieldLabel
32 // used across the Write Documentation modal (13px / 700 / ink), not an all-caps
33 // eyebrow.
34 const eyebrowStyle = { fontSize: 13, fontWeight: 700, textTransform: "capitalize", color: "var(--ink-2)" };
35 const hintStyle = { fontSize: 12, fontWeight: 500, color: "var(--muted)" };
36
37 const toggle = (ids, id) =>
38 ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id];
39
40 // ── ≤ 4 sets: segmented toggle buttons ───────────────────────────────────────
41 // `leadingIcon` (optional) renders before every chip title; when omitted the chip
42 // shows a check icon on the active item (the original behavior).
43 const ButtonGroup = ({ items, selected, onChange, disabled, leadingIcon }) => (
44 <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
45 {items.map((it) => {
46 const active = selected.includes(it.id);
47 return (
48 <button
49 key={it.id}
50 type="button"
51 disabled={disabled}
52 onClick={() => onChange(toggle(selected, it.id))}
53 title={it.title}
54 style={{
55 display: "inline-flex",
56 alignItems: "center",
57 gap: 7,
58 maxWidth: 240,
59 padding: "8px 14px",
60 borderRadius: 8,
61 cursor: disabled ? "not-allowed" : "pointer",
62 fontSize: 12.5,
63 fontWeight: 600,
64 whiteSpace: "nowrap",
65 overflow: "hidden",
66 textOverflow: "ellipsis",
67 background: active ? "var(--ai-50)" : "white",
68 color: active ? "var(--ai)" : "var(--ink-2)",
69 border: `1px solid ${active ? "var(--ai-100)" : "var(--border)"}`,
70 opacity: disabled ? 0.55 : 1,
71 transition: "background .12s, border-color .12s, color .12s",
72 }}
73 >
74 {leadingIcon
75 ? <span style={{ display: "flex", flexShrink: 0 }}>{leadingIcon}</span>
76 : (active && <IcCheck size={14} sw={2.6} />)}
77 <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
78 {it.title}
79 </span>
80 </button>
81 );
82 })}
83 </div>
84 );
85
86 // ── ≥ 5 sets: multi-select dropdown ──────────────────────────────────────────
87 const Dropdown = ({ items, selected, onChange, disabled }) => {
88 const [open, setOpen] = useState(false);
89 const ref = useRef(null);
90
91 useEffect(() => {
92 if (!open) return;
93 const onDocClick = (e) => {
94 if (ref.current && !ref.current.contains(e.target)) setOpen(false);
95 };
96 document.addEventListener("mousedown", onDocClick);
97 return () => document.removeEventListener("mousedown", onDocClick);
98 }, [open]);
99
100 const count = selected.length;
101 const summary =
102 count === 0
103 ? __("Select instructions…", "betterdocs")
104 : sprintf(
105 /* translators: %d: number of selected instruction sets. */
106 __("%d selected", "betterdocs"),
107 count
108 );
109
110 return (
111 <div ref={ref} style={{ position: "relative" }}>
112 <button
113 type="button"
114 disabled={disabled}
115 onClick={() => setOpen((v) => !v)}
116 aria-expanded={open}
117 style={{
118 width: "100%",
119 display: "flex",
120 alignItems: "center",
121 justifyContent: "space-between",
122 gap: 10,
123 border: "1px solid var(--border)",
124 background: "#fcfcfd",
125 borderRadius: 9,
126 padding: "9px 12px",
127 fontSize: 13,
128 fontWeight: 600,
129 color: count ? "var(--ink-2)" : "var(--muted)",
130 cursor: disabled ? "not-allowed" : "pointer",
131 opacity: disabled ? 0.55 : 1,
132 }}
133 >
134 <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
135 {summary}
136 </span>
137 <span
138 style={{
139 display: "flex",
140 color: "var(--muted)",
141 transform: open ? "rotate(180deg)" : "none",
142 transition: "transform .15s",
143 }}
144 >
145 <IcChevDown size={15} />
146 </span>
147 </button>
148
149 {open && (
150 <div
151 style={{
152 position: "absolute",
153 zIndex: 10,
154 top: "calc(100% + 6px)",
155 left: 0,
156 right: 0,
157 maxHeight: 240,
158 overflowY: "auto",
159 background: "white",
160 border: "1px solid var(--border)",
161 borderRadius: 10,
162 boxShadow: "0 8px 24px rgba(15,23,42,.12)",
163 padding: 6,
164 }}
165 >
166 {items.map((it) => {
167 const active = selected.includes(it.id);
168 return (
169 <button
170 key={it.id}
171 type="button"
172 onClick={() => onChange(toggle(selected, it.id))}
173 style={{
174 width: "100%",
175 display: "flex",
176 alignItems: "center",
177 gap: 10,
178 padding: "8px 10px",
179 borderRadius: 7,
180 border: "none",
181 background: active ? "var(--ai-50)" : "transparent",
182 cursor: "pointer",
183 textAlign: "left",
184 fontSize: 13,
185 fontWeight: 600,
186 color: "var(--ink-2)",
187 }}
188 >
189 <span
190 style={{
191 width: 18,
192 height: 18,
193 flexShrink: 0,
194 borderRadius: 5,
195 display: "flex",
196 alignItems: "center",
197 justifyContent: "center",
198 background: active ? "var(--ai)" : "white",
199 border: `1px solid ${active ? "var(--ai)" : "var(--border)"}`,
200 color: "white",
201 }}
202 >
203 {active && <IcCheck size={12} sw={3} />}
204 </span>
205 <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
206 {it.title}
207 </span>
208 </button>
209 );
210 })}
211 </div>
212 )}
213 </div>
214 );
215 };
216
217 // ── Public component ─────────────────────────────────────────────────────────
218 const InstructionPicker = ({
219 instructions,
220 selected = [],
221 onChange,
222 disabled = false,
223 label,
224 hint,
225 hideLabel = false,
226 leadingIcon,
227 style,
228 }) => {
229 const items = Array.isArray(instructions)
230 ? instructions.filter((i) => i && i.id)
231 : [];
232
233 if (items.length === 0) return null;
234
235 const headerLabel = label || DEFAULT_LABEL;
236 const headerHint = hint === undefined ? DEFAULT_HINT : hint; // pass "" to suppress
237 const chipIcon = leadingIcon || <IcGuide size={14} />;
238
239 return (
240 <div style={style}>
241 {!hideLabel && (
242 <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12, marginBottom: 8 }}>
243 <span style={eyebrowStyle}>{headerLabel}</span>
244 {headerHint && <span style={hintStyle}>{headerHint}</span>}
245 </div>
246 )}
247 {items.length <= BUTTON_LIMIT ? (
248 <ButtonGroup items={items} selected={selected} onChange={onChange} disabled={disabled} leadingIcon={chipIcon} />
249 ) : (
250 <Dropdown items={items} selected={selected} onChange={onChange} disabled={disabled} />
251 )}
252 </div>
253 );
254 };
255
256 export default InstructionPicker;
257