| 1 |
import { __, sprintf } from "@wordpress/i18n"; |
| 2 |
import { normalizeAIError } from "../../utils/aiErrorMessage"; |
| 3 |
import { useSelect, useDispatch } from "@wordpress/data"; |
| 4 |
import { useState, useMemo, useCallback, useEffect } from "@wordpress/element"; |
| 5 |
import apiFetch from "@wordpress/api-fetch"; |
| 6 |
|
| 7 |
import { STORE_KEY } from "./store"; |
| 8 |
import { PRESETS as FALLBACK_PRESETS, TONE_OPTIONS, LANGUAGE_OPTIONS } from "./presets"; |
| 9 |
import { applyBlockReplace, applyInsertAfter, applyInlineReplace } from "./apply"; |
| 10 |
|
| 11 |
import { |
| 12 |
Modal, TextArea, Pick, Banner, |
| 13 |
PrimaryBtn, GhostBtn, TextBtn, Spinner, AIMarkWhite, IcCheck, IcArrowR, |
| 14 |
AIcon, AIBadgeIcon, IcSummary, IcPlus, |
| 15 |
} from "../ai-shared/kit"; |
| 16 |
import { PreviewAccept } from "../ai-shared/PreviewAccept"; |
| 17 |
import InstructionPicker from "../ai-shared/InstructionPicker"; |
| 18 |
|
| 19 |
const getConfig = () => (typeof window !== "undefined" && window.betterdocsAIEdit) || {}; |
| 20 |
|
| 21 |
const resolvePrompt = (preset, optionValue) => { |
| 22 |
if (!preset || !preset.prompt) { |
| 23 |
return ""; |
| 24 |
} |
| 25 |
if (preset.needsOption && optionValue) { |
| 26 |
return preset.prompt.split("{option}").join(optionValue); |
| 27 |
} |
| 28 |
return preset.prompt; |
| 29 |
}; |
| 30 |
|
| 31 |
// ── Action icons (keyed by preset id; custom/unknown actions fall back to a spark) |
| 32 |
const Stroke = (props) => <AIcon sw={1.8} {...props} />; |
| 33 |
const ACTION_ICONS = { |
| 34 |
improve: (p) => <AIBadgeIcon size={p.size} />, |
| 35 |
shorten: (p) => <Stroke {...p}><path d="M20 4l-5 5m0 0V5m0 4h4" /><path d="M4 20l5-5m0 0v4m0-4H5" /></Stroke>, |
| 36 |
expand: (p) => <Stroke {...p}><path d="M15 9l5-5m0 0h-4m4 0v4" /><path d="M9 15l-5 5m0 0h4m-4 0v-4" /></Stroke>, |
| 37 |
simplify: (p) => <Stroke {...p}><path d="M3 21l9-9" /><path d="M15 3l1.3 2.7L19 7l-2.7 1.3L15 11l-1.3-2.7L11 7z" /></Stroke>, |
| 38 |
change_tone: (p) => <Stroke {...p}><path d="M4 6h9M17 6h3M4 12h3M11 12h9M4 18h11M19 18h1" /><circle cx="15" cy="6" r="2" /><circle cx="9" cy="12" r="2" /><circle cx="17" cy="18" r="2" /></Stroke>, |
| 39 |
translate: (p) => <Stroke {...p}><circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3c2.6 2.6 2.6 15.4 0 18M12 3c-2.6 2.6-2.6 15.4 0 18" /></Stroke>, |
| 40 |
summarize: (p) => <IcSummary size={p.size} sw={1.8} />, |
| 41 |
create_table: (p) => <Stroke {...p}><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M3 9h18M3 14h18M9 4v16M15 4v16" /></Stroke>, |
| 42 |
}; |
| 43 |
const iconFor = (id, size = 16) => (ACTION_ICONS[id] || ((p) => <AIBadgeIcon size={p.size} />))({ size }); |
| 44 |
|
| 45 |
// Solid quotation-mark glyph — the leading cue on the selected-content excerpt. |
| 46 |
const IcQuote = ({ size = 16 }) => ( |
| 47 |
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg"> |
| 48 |
<path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z" /> |
| 49 |
</svg> |
| 50 |
); |
| 51 |
|
| 52 |
// Capitalized section label — mirrors the FieldLabel used across the Write |
| 53 |
// Documentation modal (13px / 700 / ink), not an all-caps eyebrow. |
| 54 |
const Eyebrow = ({ children, style }) => ( |
| 55 |
<div style={{ fontSize: 13, fontWeight: 700, textTransform: "capitalize", color: "var(--ink-2)", ...style }}> |
| 56 |
{children} |
| 57 |
</div> |
| 58 |
); |
| 59 |
|
| 60 |
// Muted right-aligned hint sitting on the same row as an Eyebrow. |
| 61 |
const HeaderHint = ({ children }) => ( |
| 62 |
<span style={{ fontSize: 12, fontWeight: 500, color: "var(--muted)" }}>{children}</span> |
| 63 |
); |
| 64 |
|
| 65 |
// Small rounded-square icon holder — solid green when active, light grey otherwise. |
| 66 |
const IconSquare = ({ active, children }) => ( |
| 67 |
<span style={{ |
| 68 |
width: 30, height: 30, borderRadius: 8, flexShrink: 0, |
| 69 |
display: "flex", alignItems: "center", justifyContent: "center", |
| 70 |
background: active ? "var(--green)" : "#f1f3f5", |
| 71 |
color: active ? "#fff" : "var(--ink-2)", |
| 72 |
transition: "background .12s, color .12s", |
| 73 |
}}> |
| 74 |
{children} |
| 75 |
</span> |
| 76 |
); |
| 77 |
|
| 78 |
// Action card — icon square + label. The active state keeps green only as the |
| 79 |
// accent (border + icon square) over a subtle ash fill, so the grid doesn't read |
| 80 |
// as "all green". |
| 81 |
const ActionCard = ({ active, disabled, onClick, icon, children, style }) => ( |
| 82 |
<button type="button" onClick={disabled ? undefined : onClick} disabled={disabled} style={{ |
| 83 |
display: "flex", alignItems: "center", gap: 10, textAlign: "left", |
| 84 |
padding: "11px 12px", borderRadius: 12, width: "100%", |
| 85 |
cursor: disabled ? "not-allowed" : "pointer", |
| 86 |
background: active ? "var(--text-color-50, #f9fafb)" : "#fff", |
| 87 |
border: `1.5px solid ${active ? "var(--ai)" : "var(--border)"}`, |
| 88 |
color: "var(--ink-2)", |
| 89 |
fontSize: 13, fontWeight: 600, |
| 90 |
opacity: disabled ? 0.55 : 1, transition: "background .12s, border-color .12s, color .12s", |
| 91 |
...style, |
| 92 |
}}> |
| 93 |
<IconSquare active={active}>{icon}</IconSquare> |
| 94 |
{children} |
| 95 |
</button> |
| 96 |
); |
| 97 |
|
| 98 |
const AIEditModal = () => { |
| 99 |
const state = useSelect((s) => s(STORE_KEY).getState(), []); |
| 100 |
const { closeModal, setStatus, setResult, resetResult } = useDispatch(STORE_KEY); |
| 101 |
|
| 102 |
const [selectedAction, setSelectedAction] = useState("__custom"); |
| 103 |
const [instruction, setInstruction] = useState(""); |
| 104 |
const [option, setOption] = useState(TONE_OPTIONS[0].value); |
| 105 |
const [prevOption, setPrevOption] = useState(""); |
| 106 |
const [selectedInstructionIds, setSelectedInstructionIds] = useState([]); |
| 107 |
|
| 108 |
const instructionChoices = useMemo(() => { |
| 109 |
const list = getConfig().instructions; |
| 110 |
return Array.isArray(list) ? list : []; |
| 111 |
}, []); |
| 112 |
|
| 113 |
// Actions come from settings (Edit AI Personalize) via the localized config; |
| 114 |
// fall back to the built-in presets if none were provided. |
| 115 |
const presets = useMemo(() => { |
| 116 |
const list = getConfig().actions; |
| 117 |
return Array.isArray(list) && list.length ? list : FALLBACK_PRESETS; |
| 118 |
}, []); |
| 119 |
|
| 120 |
const activePreset = useMemo( |
| 121 |
() => presets.find((p) => p.id === selectedAction), |
| 122 |
[presets, selectedAction] |
| 123 |
); |
| 124 |
|
| 125 |
const onSelectPreset = useCallback((preset) => { |
| 126 |
setSelectedAction(preset.id); |
| 127 |
let nextOption = ""; |
| 128 |
if (preset.needsOption === "tone") { |
| 129 |
nextOption = TONE_OPTIONS[0].value; |
| 130 |
} else if (preset.needsOption === "language") { |
| 131 |
nextOption = LANGUAGE_OPTIONS[0].value; |
| 132 |
} |
| 133 |
if (preset.needsOption) { |
| 134 |
setOption(nextOption); |
| 135 |
} |
| 136 |
setInstruction(resolvePrompt(preset, nextOption)); |
| 137 |
setPrevOption(preset.needsOption ? nextOption : ""); |
| 138 |
}, []); |
| 139 |
|
| 140 |
const onSelectCustom = useCallback(() => { |
| 141 |
setSelectedAction("__custom"); |
| 142 |
setInstruction(""); |
| 143 |
setPrevOption(""); |
| 144 |
}, []); |
| 145 |
|
| 146 |
const onOptionChange = useCallback( |
| 147 |
(nextOption) => { |
| 148 |
setOption(nextOption); |
| 149 |
if (prevOption && nextOption !== prevOption) { |
| 150 |
setInstruction((current) => |
| 151 |
current ? current.split(prevOption).join(nextOption) : current |
| 152 |
); |
| 153 |
} |
| 154 |
setPrevOption(nextOption); |
| 155 |
}, |
| 156 |
[prevOption] |
| 157 |
); |
| 158 |
|
| 159 |
const onGenerate = useCallback(async () => { |
| 160 |
const config = getConfig(); |
| 161 |
if (!config.rest_url) { |
| 162 |
setStatus("error", __("AI Edit is not properly initialized.", "betterdocs")); |
| 163 |
return; |
| 164 |
} |
| 165 |
|
| 166 |
const body = { |
| 167 |
post_id: config.post_id, |
| 168 |
action: selectedAction === "__custom" ? "custom" : selectedAction, |
| 169 |
selection: state.selectionHtml || state.selectionText || "", |
| 170 |
selection_type: state.selectionType || "block", |
| 171 |
instruction: instruction, |
| 172 |
option: "", |
| 173 |
instruction_ids: selectedInstructionIds, |
| 174 |
}; |
| 175 |
|
| 176 |
setStatus("loading"); |
| 177 |
|
| 178 |
try { |
| 179 |
const response = await apiFetch({ |
| 180 |
url: config.rest_url, |
| 181 |
method: "POST", |
| 182 |
headers: { "X-WP-Nonce": config.rest_nonce }, |
| 183 |
data: body, |
| 184 |
}); |
| 185 |
|
| 186 |
if (response && response.success && response.data && response.data.content) { |
| 187 |
setResult(response.data.content); |
| 188 |
} else if (response && response.message) { |
| 189 |
setStatus("error", normalizeAIError(response)); |
| 190 |
} else { |
| 191 |
setStatus("error", __("Unexpected response from the server.", "betterdocs")); |
| 192 |
} |
| 193 |
} catch (e) { |
| 194 |
const message = normalizeAIError( |
| 195 |
e, |
| 196 |
__("Failed to reach the AI service. Please try again.", "betterdocs") |
| 197 |
); |
| 198 |
setStatus("error", message); |
| 199 |
} |
| 200 |
}, [state, selectedAction, activePreset, instruction, option, selectedInstructionIds, setStatus, setResult]); |
| 201 |
|
| 202 |
const onReplace = useCallback(() => { |
| 203 |
if (!state.result) return; |
| 204 |
if (state.selectionType === "inline") { |
| 205 |
applyInlineReplace(state.blockClientId, state.inlineContext, state.result); |
| 206 |
} else { |
| 207 |
applyBlockReplace(state.blockClientId, state.result); |
| 208 |
} |
| 209 |
closeModal(); |
| 210 |
}, [state, closeModal]); |
| 211 |
|
| 212 |
const onInsertAfter = useCallback(() => { |
| 213 |
if (!state.result) return; |
| 214 |
applyInsertAfter(state.blockClientId, state.result); |
| 215 |
closeModal(); |
| 216 |
}, [state, closeModal]); |
| 217 |
|
| 218 |
const onDiscard = useCallback(() => { |
| 219 |
resetResult(); |
| 220 |
}, [resetResult]); |
| 221 |
|
| 222 |
// Reset instruction selection each time the modal opens. |
| 223 |
useEffect(() => { |
| 224 |
if (state.isOpen) { |
| 225 |
setSelectedInstructionIds([]); |
| 226 |
} |
| 227 |
}, [state.isOpen]); |
| 228 |
|
| 229 |
useEffect(() => { |
| 230 |
if (!state.isOpen || !state.blockClientId) { |
| 231 |
return; |
| 232 |
} |
| 233 |
|
| 234 |
const HIGHLIGHT_CLASS = "betterdocs-ai-edit-highlight"; |
| 235 |
const STYLE_ID = "betterdocs-ai-edit-highlight-style"; |
| 236 |
const STYLE_CSS = ` |
| 237 |
.${HIGHLIGHT_CLASS} { |
| 238 |
outline: 2px solid #00B884 !important; |
| 239 |
outline-offset: 2px; |
| 240 |
border-radius: 2px; |
| 241 |
box-shadow: 0 0 0 4px rgba(0, 184, 132, 0.15); |
| 242 |
transition: outline 0.15s ease-out, box-shadow 0.15s ease-out; |
| 243 |
} |
| 244 |
`; |
| 245 |
const selector = `[data-block="${state.blockClientId}"]`; |
| 246 |
const targets = []; |
| 247 |
const docs = new Set(); |
| 248 |
|
| 249 |
const collectFromDoc = (doc) => { |
| 250 |
try { |
| 251 |
doc.querySelectorAll(selector).forEach((el) => { |
| 252 |
el.classList.add(HIGHLIGHT_CLASS); |
| 253 |
targets.push(el); |
| 254 |
if (el.ownerDocument) { |
| 255 |
docs.add(el.ownerDocument); |
| 256 |
} |
| 257 |
}); |
| 258 |
} catch (e) { } |
| 259 |
}; |
| 260 |
|
| 261 |
collectFromDoc(document); |
| 262 |
document.querySelectorAll('iframe[name="editor-canvas"]').forEach((iframe) => { |
| 263 |
try { |
| 264 |
if (iframe.contentDocument) { |
| 265 |
collectFromDoc(iframe.contentDocument); |
| 266 |
} |
| 267 |
} catch (e) { } |
| 268 |
}); |
| 269 |
|
| 270 |
docs.forEach((doc) => { |
| 271 |
if (!doc.getElementById(STYLE_ID) && doc.head) { |
| 272 |
const style = doc.createElement("style"); |
| 273 |
style.id = STYLE_ID; |
| 274 |
style.textContent = STYLE_CSS; |
| 275 |
doc.head.appendChild(style); |
| 276 |
} |
| 277 |
}); |
| 278 |
|
| 279 |
return () => { |
| 280 |
targets.forEach((el) => el.classList.remove(HIGHLIGHT_CLASS)); |
| 281 |
}; |
| 282 |
}, [state.isOpen, state.blockClientId]); |
| 283 |
|
| 284 |
if (!state.isOpen) { |
| 285 |
return null; |
| 286 |
} |
| 287 |
|
| 288 |
const optionsForActive = |
| 289 |
activePreset && activePreset.needsOption === "tone" |
| 290 |
? TONE_OPTIONS |
| 291 |
: activePreset && activePreset.needsOption === "language" |
| 292 |
? LANGUAGE_OPTIONS |
| 293 |
: null; |
| 294 |
|
| 295 |
const optionLabel = |
| 296 |
activePreset && activePreset.needsOption === "tone" |
| 297 |
? __("Tone", "betterdocs") |
| 298 |
: activePreset && activePreset.needsOption === "language" |
| 299 |
? __("Language", "betterdocs") |
| 300 |
: ""; |
| 301 |
|
| 302 |
const isStructural = !!(activePreset && activePreset.structural); |
| 303 |
const canReplaceInline = !(isStructural && state.selectionType === "inline"); |
| 304 |
|
| 305 |
const selectionPreview = state.selectionText || __("(No text)", "betterdocs"); |
| 306 |
const truncated = selectionPreview.length > 300 |
| 307 |
? selectionPreview.slice(0, 300) + "…" |
| 308 |
: selectionPreview; |
| 309 |
|
| 310 |
const isLoading = state.status === "loading"; |
| 311 |
const hasResult = state.status === "done" && state.result; |
| 312 |
|
| 313 |
const selectedActionLabel = selectedAction === "__custom" |
| 314 |
? __("Custom Prompt", "betterdocs") |
| 315 |
: (activePreset ? activePreset.label : ""); |
| 316 |
|
| 317 |
return ( |
| 318 |
<Modal open={state.isOpen} onClose={closeModal} width={600} labelledBy="bd-ai-edit-title"> |
| 319 |
{/* Header */} |
| 320 |
<div style={{ padding: "22px 26px 0" }}> |
| 321 |
<div style={{ display: "flex", alignItems: "center", gap: 13 }}> |
| 322 |
<span style={{ |
| 323 |
width: 46, height: 46, borderRadius: 13, flexShrink: 0, |
| 324 |
display: "flex", alignItems: "center", justifyContent: "center", |
| 325 |
background: "var(--green)", boxShadow: "0 2px 6px rgba(0,184,132,.35)", |
| 326 |
}}> |
| 327 |
<AIMarkWhite size={22} /> |
| 328 |
</span> |
| 329 |
<div style={{ paddingRight: 30 }}> |
| 330 |
<div id="bd-ai-edit-title" style={{ fontSize: 18, fontWeight: 800, color: "var(--ink)", letterSpacing: -0.3 }}> |
| 331 |
{__("Edit with BetterDocs AI", "betterdocs")} |
| 332 |
</div> |
| 333 |
<div style={{ fontSize: 13, color: "var(--muted)", marginTop: 2 }}> |
| 334 |
{__("Refine your selected content", "betterdocs")} |
| 335 |
</div> |
| 336 |
</div> |
| 337 |
</div> |
| 338 |
</div> |
| 339 |
|
| 340 |
{/* Body */} |
| 341 |
<div style={{ padding: "18px 26px 24px", overflowY: "auto" }}> |
| 342 |
{!hasResult && ( |
| 343 |
<> |
| 344 |
{/* Selected content — a quoted excerpt, not an input: an |
| 345 |
ash-filled callout with a leading quote glyph so it |
| 346 |
clearly reads as "the text you selected". */} |
| 347 |
<Eyebrow style={{ marginBottom: 8 }}>{__("Selected content", "betterdocs")}</Eyebrow> |
| 348 |
<div style={{ |
| 349 |
display: "flex", gap: 11, alignItems: "flex-start", |
| 350 |
background: "var(--text-color-50, #f9fafb)", |
| 351 |
border: "1px solid var(--text-color-200, #eaecf0)", |
| 352 |
borderRadius: 10, padding: "13px 15px", marginBottom: 20, |
| 353 |
}}> |
| 354 |
<span style={{ flexShrink: 0, marginTop: 1, color: "var(--muted-2)", display: "flex" }}> |
| 355 |
<IcQuote size={16} /> |
| 356 |
</span> |
| 357 |
<div style={{ |
| 358 |
flex: 1, minWidth: 0, |
| 359 |
fontSize: 13, lineHeight: 1.6, color: "var(--ink-2)", |
| 360 |
maxHeight: 120, overflow: "auto", whiteSpace: "pre-wrap", wordBreak: "break-word", |
| 361 |
}}> |
| 362 |
{truncated} |
| 363 |
</div> |
| 364 |
</div> |
| 365 |
|
| 366 |
{/* Actions */} |
| 367 |
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12, marginBottom: 10 }}> |
| 368 |
<Eyebrow>{__("Choose an action", "betterdocs")}</Eyebrow> |
| 369 |
{selectedActionLabel && ( |
| 370 |
<HeaderHint> |
| 371 |
{sprintf(/* translators: %s: selected action name. */ __("Selected: %s", "betterdocs"), selectedActionLabel)} |
| 372 |
</HeaderHint> |
| 373 |
)} |
| 374 |
</div> |
| 375 |
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 10 }}> |
| 376 |
{presets.map((preset) => ( |
| 377 |
<ActionCard key={preset.id} active={selectedAction === preset.id} |
| 378 |
disabled={isLoading} onClick={() => onSelectPreset(preset)} |
| 379 |
icon={iconFor(preset.id)}> |
| 380 |
{preset.label} |
| 381 |
</ActionCard> |
| 382 |
))} |
| 383 |
</div> |
| 384 |
|
| 385 |
{/* Custom instruction — full-width card */} |
| 386 |
<ActionCard active={selectedAction === "__custom"} disabled={isLoading} |
| 387 |
onClick={onSelectCustom} icon={<IcPlus size={16} sw={2} />} style={{ marginTop: 10 }}> |
| 388 |
<span style={{ flex: 1 }}>{__("Custom Prompt", "betterdocs")}</span> |
| 389 |
<span style={{ fontSize: 12, fontWeight: 500, color: "var(--muted)" }}> |
| 390 |
{__("Tell AI exactly what you want", "betterdocs")} |
| 391 |
</span> |
| 392 |
</ActionCard> |
| 393 |
|
| 394 |
{optionsForActive && ( |
| 395 |
<div style={{ marginTop: 18 }}> |
| 396 |
<Eyebrow style={{ marginBottom: 8 }}>{optionLabel}</Eyebrow> |
| 397 |
<Pick value={option} onChange={onOptionChange} disabled={isLoading} |
| 398 |
options={optionsForActive} style={{ width: "100%" }} /> |
| 399 |
</div> |
| 400 |
)} |
| 401 |
|
| 402 |
<Eyebrow style={{ marginTop: 20, marginBottom: 8 }}>{__("Prompt", "betterdocs")}</Eyebrow> |
| 403 |
<TextArea value={instruction} onChange={setInstruction} rows={5} disabled={isLoading} |
| 404 |
placeholder={__("Describe exactly what you want the AI to do.", "betterdocs")} /> |
| 405 |
<div style={{ fontSize: 12.5, color: "var(--muted)", marginTop: 8, lineHeight: 1.5 }}> |
| 406 |
{selectedAction === "__custom" |
| 407 |
? __("Describe exactly what you want the AI to do.", "betterdocs") |
| 408 |
: __("Review and edit the prompt before generating.", "betterdocs")} |
| 409 |
</div> |
| 410 |
|
| 411 |
{instructionChoices.length > 0 && ( |
| 412 |
<InstructionPicker |
| 413 |
instructions={instructionChoices} |
| 414 |
selected={selectedInstructionIds} |
| 415 |
onChange={setSelectedInstructionIds} |
| 416 |
disabled={isLoading} |
| 417 |
style={{ marginTop: 20 }} |
| 418 |
/> |
| 419 |
)} |
| 420 |
|
| 421 |
{state.status === "error" && state.error && ( |
| 422 |
<Banner tone="error" style={{ marginTop: 18 }}>{state.error}</Banner> |
| 423 |
)} |
| 424 |
|
| 425 |
{/* footer */} |
| 426 |
<div style={{ |
| 427 |
display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 10, |
| 428 |
marginTop: 22, paddingTop: 18, borderTop: "1px solid var(--border-soft)", |
| 429 |
}}> |
| 430 |
<TextBtn onClick={closeModal}>{__("Close", "betterdocs")}</TextBtn> |
| 431 |
<PrimaryBtn icon={isLoading ? <Spinner size={15} color="#fff" /> : <AIMarkWhite size={16} />} |
| 432 |
onClick={onGenerate} disabled={isLoading || !instruction.trim()}> |
| 433 |
{isLoading ? __("Generating…", "betterdocs") : __("Generate", "betterdocs")} |
| 434 |
</PrimaryBtn> |
| 435 |
</div> |
| 436 |
</> |
| 437 |
)} |
| 438 |
|
| 439 |
{hasResult && ( |
| 440 |
<PreviewAccept |
| 441 |
mode="replace" |
| 442 |
status="ready" |
| 443 |
suggestion={state.result} |
| 444 |
actions={ |
| 445 |
<> |
| 446 |
<TextBtn onClick={onDiscard} style={{ marginRight: "auto" }}> |
| 447 |
{__("Discard & Retry", "betterdocs")} |
| 448 |
</TextBtn> |
| 449 |
<GhostBtn icon={<IcArrowR size={15} />} onClick={onInsertAfter}> |
| 450 |
{__("Insert After", "betterdocs")} |
| 451 |
</GhostBtn> |
| 452 |
{canReplaceInline && ( |
| 453 |
<PrimaryBtn icon={<IcCheck size={16} sw={2.4} />} onClick={onReplace}> |
| 454 |
{__("Replace Selection", "betterdocs")} |
| 455 |
</PrimaryBtn> |
| 456 |
)} |
| 457 |
</> |
| 458 |
} |
| 459 |
/> |
| 460 |
)} |
| 461 |
</div> |
| 462 |
</Modal> |
| 463 |
); |
| 464 |
}; |
| 465 |
|
| 466 |
export default AIEditModal; |
| 467 |
|