| 1 |
import React, { useState, useEffect } from "react"; |
| 2 |
import { __ } from "@wordpress/i18n"; |
| 3 |
import { normalizeAIError } from "../../../utils/aiErrorMessage"; |
| 4 |
import Modal from "react-modal"; |
| 5 |
|
| 6 |
import { getSettingsData, generateFaqAnswer } from "../utils/function"; |
| 7 |
|
| 8 |
// Custom modal styles |
| 9 |
const customStyles = { |
| 10 |
overlay: { |
| 11 |
position: "fixed", |
| 12 |
top: 0, |
| 13 |
left: 0, |
| 14 |
right: 0, |
| 15 |
bottom: 0, |
| 16 |
backgroundColor: "rgba(35, 40, 45, 0.5)", |
| 17 |
zIndex: 9990, |
| 18 |
padding: 50, |
| 19 |
paddingTop: 70, |
| 20 |
display: "flex", |
| 21 |
flexDirection: "column", |
| 22 |
overflow: "auto", |
| 23 |
}, |
| 24 |
content: { |
| 25 |
width: "40rem", |
| 26 |
maxWidth: "90%", |
| 27 |
position: "static", |
| 28 |
height: "auto", |
| 29 |
overflow: "initial", |
| 30 |
margin: "auto", |
| 31 |
}, |
| 32 |
}; |
| 33 |
const FaqAIModalForm = ({ |
| 34 |
open, |
| 35 |
setShowFaqAIModalForm, |
| 36 |
// reFetchCategories, |
| 37 |
// setShowCategoryModal, |
| 38 |
// updateCategory, |
| 39 |
// setUpdateCategory, |
| 40 |
setFormFields, |
| 41 |
formFields, |
| 42 |
// handleAnswerChange |
| 43 |
}) => { |
| 44 |
const [generatedResponse, setGeneratedResponse] = useState(formFields); |
| 45 |
const [generatedAnswer, setGeneratedAnswer] = useState(null); |
| 46 |
const [error, setError] = useState(null); |
| 47 |
const [isLoading, setIsLoading] = useState(false); |
| 48 |
const [contentGenerated, setContentGenerated] = useState(false); |
| 49 |
|
| 50 |
const [settings, setSettings] = useState(null); |
| 51 |
|
| 52 |
useEffect(() => { |
| 53 |
getSettingsData().then((getSettings) => { |
| 54 |
setSettings(getSettings); |
| 55 |
}).catch(function (error) { |
| 56 |
console.log(error); |
| 57 |
}); |
| 58 |
}, []); |
| 59 |
|
| 60 |
const resetErrors = () => { |
| 61 |
setError(null); |
| 62 |
}; |
| 63 |
|
| 64 |
const handleCancel = () => { |
| 65 |
resetErrors(); |
| 66 |
// setShowCategoryModal(false); |
| 67 |
setShowFaqAIModalForm(false); |
| 68 |
// setUpdateCategory(null); |
| 69 |
}; |
| 70 |
|
| 71 |
|
| 72 |
const highlightKeywords = (inputString, keywords) => { |
| 73 |
let modifiedString = inputString.replace(/<(h\d)(.*?)>(.*?)<\/\1>/g, '<$1$2><span className="highlight">$3</span></$1>'); |
| 74 |
|
| 75 |
const keywordArray = keywords.split(',').map(keyword => keyword.trim()); |
| 76 |
|
| 77 |
keywordArray.forEach(keyword => { |
| 78 |
modifiedString = modifiedString.replace(new RegExp(`\\b(${keyword})\\b`, 'gi'), '<span className="highlight">$1</span>'); |
| 79 |
}); |
| 80 |
|
| 81 |
return modifiedString; |
| 82 |
} |
| 83 |
|
| 84 |
const getAPIKey = (settings) => { |
| 85 |
if (!settings) { |
| 86 |
return ''; |
| 87 |
} |
| 88 |
|
| 89 |
// Mirror the server-side ProviderFactory::key_field_for(): the key lives in |
| 90 |
// the field for the *selected* AI platform, not always OpenAI. OpenAI keeps |
| 91 |
// the legacy `ai_autowrite_api_key`; every other platform (gemini, claude, |
| 92 |
// deepseek, …) uses `ai_api_key_<platform>`. Checking only the OpenAI field |
| 93 |
// blocked "Write with AI" whenever another provider was configured. |
| 94 |
const platform = settings.ai_platform || 'openai'; |
| 95 |
const field = platform === 'openai' ? 'ai_autowrite_api_key' : `ai_api_key_${platform}`; |
| 96 |
|
| 97 |
return settings[field] || ''; |
| 98 |
} |
| 99 |
|
| 100 |
const handleSubmit = async (event) => { |
| 101 |
event.preventDefault(); |
| 102 |
|
| 103 |
const question = document.getElementById('betterdocs-ai-title').value; |
| 104 |
const keywords = document.getElementById('betterdocs-ai-keyword').value; |
| 105 |
|
| 106 |
try { |
| 107 |
setIsLoading(true); |
| 108 |
setError(null); |
| 109 |
|
| 110 |
// The prompt build, model selection and "Answer:" cleanup now happen |
| 111 |
// server-side; the OpenAI API key never leaves the server. |
| 112 |
const res = await generateFaqAnswer({ question, keywords }); |
| 113 |
const answer = res?.data?.answer || ""; |
| 114 |
|
| 115 |
setIsLoading(false); |
| 116 |
|
| 117 |
if (!answer) { |
| 118 |
setContentGenerated(false); |
| 119 |
setError(__("The AI returned no answer. Try again or rephrase your question.", "betterdocs")); |
| 120 |
return; |
| 121 |
} |
| 122 |
|
| 123 |
setContentGenerated(true); |
| 124 |
setGeneratedResponse(state => ({ |
| 125 |
...state, |
| 126 |
question: question, |
| 127 |
aiAnswer: answer, |
| 128 |
})); |
| 129 |
setGeneratedAnswer(answer); |
| 130 |
} catch (error) { |
| 131 |
setIsLoading(false); |
| 132 |
setContentGenerated(false); |
| 133 |
setError(normalizeAIError(error)); |
| 134 |
} |
| 135 |
}; |
| 136 |
|
| 137 |
|
| 138 |
const handleKeep = () => { |
| 139 |
// Set `answer` (the field actually saved as post_content) directly to the AI |
| 140 |
// answer — do NOT rely on the editor's async aiValue effect to copy it across. |
| 141 |
// If that effect races the editor's init or the answer is unchanged, `answer` |
| 142 |
// stayed empty and the FAQ saved blank ("content not being saved"). aiAnswer is |
| 143 |
// kept so the editor still renders the content visually. |
| 144 |
setFormFields({ |
| 145 |
...generatedResponse, |
| 146 |
answer: generatedResponse?.aiAnswer || generatedResponse?.answer || "", |
| 147 |
}); |
| 148 |
handleCancel(); |
| 149 |
}; |
| 150 |
|
| 151 |
return ( |
| 152 |
<Modal |
| 153 |
isOpen={open} |
| 154 |
className="bd-faq-ctg-modal" |
| 155 |
onRequestClose={() => handleCancel()} |
| 156 |
style={customStyles} |
| 157 |
> |
| 158 |
<form |
| 159 |
className={settings && !getAPIKey(settings) ? "disabled-input-field" : ""} |
| 160 |
onSubmit={handleSubmit} |
| 161 |
> |
| 162 |
<div className="bd-faq-ctg-header"> |
| 163 |
<span |
| 164 |
className="bd-faq-stat-card__icon" |
| 165 |
style={{ "--bd-icon-tint": "#7f56d9", "--bd-icon-tint-rgb": "127, 86, 217" }} |
| 166 |
> |
| 167 |
<svg |
| 168 |
xmlns="http://www.w3.org/2000/svg" |
| 169 |
width={17} |
| 170 |
height={17} |
| 171 |
viewBox="0 0 32 32" |
| 172 |
fill="none" |
| 173 |
> |
| 174 |
<path |
| 175 |
d="M15.8322 30.1765L8.97508 30.7708L9.56936 23.9136L23.8322 9.74219C24.0451 9.52448 24.2993 9.3515 24.58 9.23341C24.8606 9.11531 25.162 9.05447 25.4665 9.05447C25.771 9.05447 26.0724 9.11531 26.3531 9.23341C26.6337 9.3515 26.8879 9.52448 27.1008 9.74219L30.0037 12.6679C30.2176 12.8805 30.3874 13.1334 30.5033 13.4119C30.6192 13.6904 30.6788 13.9891 30.6788 14.2908C30.6788 14.5924 30.6192 14.8911 30.5033 15.1696C30.3874 15.4481 30.2176 15.701 30.0037 15.9136L15.8322 30.1765ZM1.92593 9.07933C1.12365 8.9399 1.12365 7.7879 1.92593 7.64848C3.34647 7.40124 4.6612 6.73658 5.70251 5.73923C6.74382 4.74188 7.46455 3.45703 7.77279 2.04848L7.82079 1.82676C7.99451 1.03591 9.12365 1.02905 9.30651 1.82219L9.36365 2.07819C9.68319 3.48048 10.4099 4.75709 11.4526 5.74772C12.4953 6.73834 13.8074 7.39881 15.2242 7.64619C16.0311 7.78333 16.0311 8.94219 15.2242 9.0839C13.8073 9.33071 12.4949 9.99066 11.4518 10.9809C10.4087 11.9711 9.68146 13.2474 9.36136 14.6496L9.30193 14.9079C9.12136 15.6988 7.99222 15.6942 7.81851 14.901L7.77279 14.6793C7.46424 13.2702 6.74284 11.9849 5.70064 10.9874C4.65845 9.99003 3.34273 9.32574 1.92136 9.07933H1.92593Z" |
| 176 |
stroke="currentColor" |
| 177 |
strokeWidth="2.28571" |
| 178 |
strokeLinecap="round" |
| 179 |
strokeLinejoin="round" |
| 180 |
/> |
| 181 |
</svg> |
| 182 |
</span> |
| 183 |
<div className="bd-faq-ctg-heading"> |
| 184 |
<h3 className="bd-faq-ctg-title"> |
| 185 |
{__("Write FAQ with BetterDocs AI", "betterdocs")} |
| 186 |
</h3> |
| 187 |
<p className="bd-faq-ctg-subtitle"> |
| 188 |
{__("Generate FAQs effortlessly with BetterDocs AI. Input your question and relevant keywords and let the system generate FAQs tailored to your needs.", "betterdocs")} |
| 189 |
</p> |
| 190 |
</div> |
| 191 |
</div> |
| 192 |
|
| 193 |
<div className="bd-faq-ctg-body"> |
| 194 |
{settings && !getAPIKey(settings) && ( |
| 195 |
<div id="betterdocs-ai-message"> |
| 196 |
<div className="warning-message"> |
| 197 |
<span className="dashicons dashicons-warning"></span> |
| 198 |
<div> |
| 199 |
{__("Please insert your", "betterdocs")}{" "} |
| 200 |
<a |
| 201 |
target="_blank" |
| 202 |
href="/wp-admin/admin.php?page=betterdocs-settings#betterdocs-ai" |
| 203 |
> |
| 204 |
{__("AI API Key", "betterdocs")} |
| 205 |
</a>{" "} |
| 206 |
{__('to use this "Write with AI" feature.', "betterdocs")} |
| 207 |
</div> |
| 208 |
</div> |
| 209 |
</div> |
| 210 |
)} |
| 211 |
|
| 212 |
{error && ( |
| 213 |
<div id="betterdocs-ai-error-message"> |
| 214 |
<div className="warning-message"> |
| 215 |
<span className="dashicons dashicons-warning"></span> |
| 216 |
<div className="footer-message">{error}</div> |
| 217 |
</div> |
| 218 |
</div> |
| 219 |
)} |
| 220 |
|
| 221 |
<div className="bd-faq-cgt-field"> |
| 222 |
<label className="bd-faq-label" htmlFor="betterdocs-ai-title"> |
| 223 |
{__("Question", "betterdocs")} |
| 224 |
</label> |
| 225 |
<input |
| 226 |
type="text" |
| 227 |
className="bd-faq-input" |
| 228 |
placeholder={__("Insert your question to generate a relevant response.", "betterdocs")} |
| 229 |
id="betterdocs-ai-title" |
| 230 |
name="betterdocs-ai-title" |
| 231 |
required |
| 232 |
value={formFields?.question} |
| 233 |
onChange={(e) => |
| 234 |
setFormFields((state) => ({ |
| 235 |
...state, |
| 236 |
question: e.target.value.replace(/^\s+/, ""), |
| 237 |
})) |
| 238 |
} |
| 239 |
/> |
| 240 |
</div> |
| 241 |
|
| 242 |
<div className="bd-faq-cgt-field"> |
| 243 |
<label className="bd-faq-label" htmlFor="betterdocs-ai-keyword"> |
| 244 |
{__("Keywords", "betterdocs")} |
| 245 |
</label> |
| 246 |
<input |
| 247 |
type="text" |
| 248 |
className="bd-faq-input" |
| 249 |
placeholder={__("Add keywords to generate precise and relevant FAQs (comma-separated).", "betterdocs")} |
| 250 |
id="betterdocs-ai-keyword" |
| 251 |
name="betterdocs-ai-keyword" |
| 252 |
required |
| 253 |
/> |
| 254 |
</div> |
| 255 |
|
| 256 |
{generatedAnswer && ( |
| 257 |
<div className="bd-faq-cgt-field generated-answer"> |
| 258 |
<label className="bd-faq-label"> |
| 259 |
{__("Generated FAQ", "betterdocs")} |
| 260 |
</label> |
| 261 |
<div className="betterdocs-ai-content-wrapper"> |
| 262 |
<p className="betterdocs-ai-content" dangerouslySetInnerHTML={{ __html: generatedAnswer }} /> |
| 263 |
</div> |
| 264 |
</div> |
| 265 |
)} |
| 266 |
</div> |
| 267 |
|
| 268 |
<div className="bd-faq-ctg-footer"> |
| 269 |
<button |
| 270 |
type="submit" |
| 271 |
className={`bd-faq-button ${contentGenerated ? "button-ghost" : "button-success"}`} |
| 272 |
disabled={isLoading} |
| 273 |
> |
| 274 |
<span className="bd-faq-text"> |
| 275 |
{isLoading ? __("Generating…", "betterdocs") : contentGenerated ? __("Regenerate FAQ", "betterdocs") : __("Generate FAQ", "betterdocs")} |
| 276 |
</span> |
| 277 |
</button> |
| 278 |
|
| 279 |
{contentGenerated && ( |
| 280 |
<button |
| 281 |
type="button" |
| 282 |
className="bd-faq-button button-success" |
| 283 |
onClick={() => handleKeep(generatedResponse)} |
| 284 |
> |
| 285 |
<span className="bd-faq-text">{__("Keep", "betterdocs")}</span> |
| 286 |
</button> |
| 287 |
)} |
| 288 |
</div> |
| 289 |
</form> |
| 290 |
|
| 291 |
<button |
| 292 |
type="button" |
| 293 |
className="bd-faq-modal-close-button" |
| 294 |
onClick={() => handleCancel()} |
| 295 |
> |
| 296 |
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 297 |
<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" /> |
| 298 |
</svg> |
| 299 |
</button> |
| 300 |
</Modal> |
| 301 |
); |
| 302 |
}; |
| 303 |
|
| 304 |
//Modal.setAppElement("#betterdocsFaqBuilder"); |
| 305 |
|
| 306 |
export default FaqAIModalForm; |
| 307 |
|