PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / src / admin / components / pages / Settings.js

Settings.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/admin/components/pages/Settings.js

785 lines 40.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Settings Page Component
3 *
4 * Plugin settings and configuration
5 *
6 * @package ThinkRank
7 * @since 1.0.0
8 */
9
10 import { __ } from '@wordpress/i18n';
11 import { TextControl, SelectControl, Notice, Spinner, CheckboxControl } from '@wordpress/components';
12 import { useState, useEffect } from '@wordpress/element';
13 import apiFetch from '@wordpress/api-fetch';
14 import ModelBadge from '../shared/ModelBadge';
15
16 // AI Provider Icons (using WordPress Dashicons as fallback)
17 const OpenAIIcon = () => (
18 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
19 <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/>
20 </svg>
21 );
22
23 const ClaudeIcon = () => (
24 <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
25 <path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fillRule="nonzero"/>
26 </svg>
27 );
28
29 const GeminiIcon = () => (
30 <svg height="1em" style={{flex:'none',lineHeight:1}} viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg">
31 <title>Gemini</title>
32 <path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"/>
33 <defs>
34 <linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12">
35 <stop stopColor="#08B962"/>
36 <stop offset="1" stopColor="#08B962" stopOpacity="0"/>
37 </linearGradient>
38 <linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11">
39 <stop stopColor="#F94543"/>
40 <stop offset="1" stopColor="#F94543" stopOpacity="0"/>
41 </linearGradient>
42 <linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12">
43 <stop stopColor="#FABC12"/>
44 <stop offset=".46" stopColor="#FABC12" stopOpacity="0"/>
45 </linearGradient>
46 </defs>
47 </svg>
48 );
49
50 /**
51 * Settings Component
52 */
53 const Settings = () => {
54 const [apiKey, setApiKey] = useState('');
55 const [aiProvider, setAiProvider] = useState('openai');
56 const [selectedModel, setSelectedModel] = useState('');
57 // Provider-specific model states to avoid cross-provider bleed
58 const [openaiModelState, setOpenaiModelState] = useState('gpt-5-nano');
59 const [claudeModelState, setClaudeModelState] = useState('claude-3-7-sonnet-latest');
60 const [geminiModelState, setGeminiModelState] = useState('gemini-2.5-flash');
61 const [isSaving, setIsSaving] = useState(false);
62 const [isLoading, setIsLoading] = useState(true);
63 const [isTesting, setIsTesting] = useState(false);
64 const [notice, setNotice] = useState(null);
65 const [hasExistingKey, setHasExistingKey] = useState(false);
66 const [deleteDataOnUninstall, setDeleteDataOnUninstall] = useState(false);
67
68 // Model options for each provider (July 2025)
69 const modelOptions = {
70 openai: [
71 { value: 'gpt-5-nano', label: 'GPT‑5‑nano (Default – fastest, lowest cost)' },
72 { value: 'gpt-5-mini', label: 'GPT‑5‑mini (Balanced quality/cost)' },
73 { value: 'gpt-5', label: 'GPT‑5 (Highest quality)' },
74 { value: 'gpt-4o', label: 'GPT‑4o (Compatibility)' }
75 ],
76 claude: [
77 { value: 'claude-sonnet-4-0', label: 'Claude 4 Sonnet (Latest)' },
78 { value: 'claude-opus-4-0', label: 'Claude 4 Opus (Latest)' },
79 { value: 'claude-3-7-sonnet-latest', label: 'Claude 3.7 Sonnet (Recommended)' },
80 { value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet' },
81 { value: 'claude-3-5-haiku-latest', label: 'Claude 3.5 Haiku (Fast)' }
82 ],
83 gemini: [
84 { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash (Recommended)' },
85 { value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash-Lite (Fastest)' },
86 { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
87 { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }
88 ]
89 };
90
91 // Track if initial load is complete to prevent duplicate API calls
92 const [initialLoadComplete, setInitialLoadComplete] = useState(false);
93
94 // Load settings on component mount
95 useEffect(() => {
96 loadSettings();
97 }, []);
98
99 // Track the previous provider to detect actual user changes
100 const [previousProvider, setPreviousProvider] = useState(null);
101
102 // Handle provider changes (only for user-initiated changes)
103 useEffect(() => {
104 // Skip if this is the initial load, provider not set, or no previous provider to compare
105 if (!initialLoadComplete || !aiProvider || previousProvider === null) {
106 setPreviousProvider(aiProvider);
107 return;
108 }
109
110 // Only proceed if this is an actual provider change (user-initiated)
111 if (previousProvider !== aiProvider) {
112 // Clear API key field when switching providers to avoid confusion
113 setApiKey('');
114 setHasExistingKey(false);
115
116 // Set selectedModel to the appropriate provider-specific model
117 if (aiProvider === 'openai') {
118 setSelectedModel(openaiModelState);
119 } else if (aiProvider === 'claude') {
120 setSelectedModel(claudeModelState);
121 } else if (aiProvider === 'gemini') {
122 setSelectedModel(geminiModelState);
123 }
124
125 // Show "switched" message only for user-initiated changes
126 const providerName = aiProvider === 'openai' ? 'OpenAI' : aiProvider === 'claude' ? 'Claude' : 'Gemini';
127 setNotice({
128 type: 'info',
129 message: __(`Switched to ${providerName}. Please enter your API key if you haven't already.`, 'thinkrank')
130 });
131
132 // Update previous provider
133 setPreviousProvider(aiProvider);
134 }
135 }, [aiProvider, initialLoadComplete, previousProvider, openaiModelState, claudeModelState, geminiModelState]);
136
137 // API key format validation
138 const validateApiKey = (key, provider) => {
139 if (!key || !key.trim()) return { valid: true, message: '' };
140
141 if (provider === 'openai') {
142 if (!key.startsWith('sk-')) {
143 return {
144 valid: false,
145 message: __('OpenAI API keys should start with "sk-"', 'thinkrank')
146 };
147 }
148 if (key.length < 20) {
149 return {
150 valid: false,
151 message: __('OpenAI API key appears too short', 'thinkrank')
152 };
153 }
154 } else if (provider === 'claude') {
155 if (!key.startsWith('sk-ant-')) {
156 return {
157 valid: false,
158 message: __('Claude API keys should start with "sk-ant-"', 'thinkrank')
159 };
160 }
161 if (key.length < 30) {
162 return {
163 valid: false,
164 message: __('Claude API key appears too short', 'thinkrank')
165 };
166 }
167 } else if (provider === 'gemini') {
168 if (!key.startsWith('AIza')) {
169 return {
170 valid: false,
171 message: __('Gemini API keys should start with "AIza"', 'thinkrank')
172 };
173 }
174 if (key.length < 30) {
175 return {
176 valid: false,
177 message: __('Gemini API key appears too short', 'thinkrank')
178 };
179 }
180 }
181
182 return { valid: true, message: '' };
183 };
184
185 const loadSettings = async () => {
186 setIsLoading(true);
187 try {
188 const response = await apiFetch({
189 path: '/thinkrank/v1/settings',
190 });
191
192 // Check if response has the expected structure
193 const settings = response.success ? response.data : response;
194
195 if (settings) {
196 // Always use saved provider from backend (this is the source of truth)
197 const savedProvider = settings.ai_provider || 'openai';
198 const currentApiKey = settings[`${savedProvider}_api_key`] || '';
199
200 // Check if we have a masked API key (indicates existing key)
201 const hasKey = currentApiKey && currentApiKey.includes('••••');
202 setHasExistingKey(hasKey);
203
204 // Don't show masked key in input
205 setApiKey(hasKey ? '' : currentApiKey);
206
207 // Always sync frontend state with backend settings
208 setAiProvider(savedProvider);
209 setPreviousProvider(savedProvider); // Set initial previous provider
210
211 // Set all provider-specific model states from backend settings
212 const openaiModel = settings.openai_model || 'gpt-5-nano';
213 const claudeModel = settings.claude_model || 'claude-3-7-sonnet-latest';
214 const geminiModel = settings.gemini_model || 'gemini-2.5-flash';
215
216 setOpenaiModelState(openaiModel);
217 setClaudeModelState(claudeModel);
218 setGeminiModelState(geminiModel);
219
220 // Set selectedModel based on current provider
221 if (savedProvider === 'claude') {
222 setSelectedModel(claudeModel);
223 } else if (savedProvider === 'gemini') {
224 setSelectedModel(geminiModel);
225 } else {
226 setSelectedModel(openaiModel);
227 }
228
229 // Set delete data on uninstall setting (defaults to false - preserve data)
230 setDeleteDataOnUninstall(settings.keep_data_on_uninstall === false);
231
232 // Show appropriate notice based on API key status
233 if (!hasKey) {
234 const providerName = savedProvider === 'openai' ? 'OpenAI' : savedProvider === 'claude' ? 'Claude' : 'Gemini';
235 setNotice({
236 type: 'warning',
237 message: __(`No ${providerName} API key found. Please enter your API key to use this provider.`, 'thinkrank')
238 });
239 } else {
240 // Clear any existing notices if key exists
241 setNotice(null);
242 }
243 }
244 } catch (error) {
245 console.error('Failed to load settings:', error);
246 setNotice({
247 type: 'error',
248 message: __('Failed to load settings.', 'thinkrank')
249 });
250 } finally {
251 setIsLoading(false);
252 setInitialLoadComplete(true); // Mark initial load as complete
253 }
254 };
255
256 const handleSave = async () => {
257 // Check if we have an API key for the selected provider
258 if (!apiKey.trim() && !hasExistingKey) {
259 const providerName = aiProvider === 'openai' ? 'OpenAI' : aiProvider === 'claude' ? 'Claude' : 'Gemini';
260 setNotice({
261 type: 'error',
262 message: __(`Please enter a ${providerName} API key for the selected provider.`, 'thinkrank')
263 });
264 return;
265 }
266
267 // Validate API key format if provided
268 if (apiKey.trim()) {
269 const validation = validateApiKey(apiKey, aiProvider);
270 if (!validation.valid) {
271 setNotice({
272 type: 'error',
273 message: validation.message
274 });
275 return;
276 }
277 }
278
279 setIsSaving(true);
280 setNotice(null);
281
282 try {
283 const saveData = {
284 ai_provider: aiProvider,
285 keep_data_on_uninstall: !deleteDataOnUninstall, // Invert because UI asks "delete data"
286 };
287
288 // Include API key if user entered a new one
289 if (apiKey.trim()) {
290 saveData[`${aiProvider}_api_key`] = apiKey;
291 }
292
293 // Include model selection (use provider-specific state)
294 if (aiProvider === 'claude') {
295 saveData.claude_model = claudeModelState;
296 } else if (aiProvider === 'gemini') {
297 saveData.gemini_model = geminiModelState;
298 } else {
299 saveData.openai_model = openaiModelState;
300 }
301
302 const response = await apiFetch({
303 path: '/thinkrank/v1/settings',
304 method: 'POST',
305 data: saveData,
306 });
307
308 if (response.success) {
309 setNotice({
310 type: 'success',
311 message: __('Settings saved successfully!', 'thinkrank')
312 });
313
314 // If we saved a new API key, mark that we have one and clear input
315 if (apiKey.trim()) {
316 setHasExistingKey(true);
317 setApiKey('');
318 }
319
320 // Refresh the settings to ensure UI is in sync
321 await loadSettings();
322 } else {
323 setNotice({
324 type: 'error',
325 message: response.message || __('Failed to save settings.', 'thinkrank')
326 });
327 }
328 } catch (error) {
329 console.error('Failed to save settings:', error);
330 setNotice({
331 type: 'error',
332 message: __('Failed to save settings. Please try again.', 'thinkrank')
333 });
334 } finally {
335 setIsSaving(false);
336 }
337 };
338
339 const handleTestConnection = async () => {
340 if (!apiKey.trim()) {
341 setNotice({
342 type: 'error',
343 message: __('Please enter an API key first.', 'thinkrank')
344 });
345 return;
346 }
347
348 // Validate API key format before testing
349 const validation = validateApiKey(apiKey, aiProvider);
350 if (!validation.valid) {
351 setNotice({
352 type: 'error',
353 message: validation.message
354 });
355 return;
356 }
357
358 setIsTesting(true);
359 setNotice(null);
360
361 try {
362 const response = await apiFetch({
363 path: '/thinkrank/v1/ai/test-connection',
364 method: 'POST',
365 data: {
366 api_key: apiKey,
367 provider: aiProvider,
368 },
369 });
370
371 if (response && response.success) {
372 setNotice({
373 type: 'success',
374 message: __('API connection successful!', 'thinkrank')
375 });
376 } else {
377 const errorMessage = response?.message ||
378 __('API connection failed. Please check your API key and try again.', 'thinkrank');
379 setNotice({
380 type: 'error',
381 message: errorMessage
382 });
383 }
384 } catch (error) {
385 console.error('Failed to test connection:', error);
386
387 // Better error handling for different types of failures
388 let errorMessage = __('Failed to test API connection.', 'thinkrank');
389
390 if (error.message) {
391 if (error.message.includes('fetch')) {
392 errorMessage = __('Network error. Please check your internet connection.', 'thinkrank');
393 } else if (error.message.includes('401') || error.message.includes('403')) {
394 errorMessage = __('Invalid API key. Please check your credentials.', 'thinkrank');
395 } else if (error.message.includes('429')) {
396 errorMessage = __('Rate limit exceeded. Please try again later.', 'thinkrank');
397 } else {
398 errorMessage = `${__('Connection failed:', 'thinkrank')} ${error.message}`;
399 }
400 }
401
402 setNotice({
403 type: 'error',
404 message: errorMessage
405 });
406 } finally {
407 setIsTesting(false);
408 }
409 };
410
411 const handleClearApiKey = async () => {
412 if (!hasExistingKey) {
413 setNotice({
414 type: 'warning',
415 message: __('No API key is currently saved.', 'thinkrank')
416 });
417 return;
418 }
419
420 if (!confirm(__('Are you sure you want to clear the saved API key? This action cannot be undone.', 'thinkrank'))) {
421 return;
422 }
423
424 setIsSaving(true);
425 setNotice(null);
426
427 try {
428 const response = await apiFetch({
429 path: '/thinkrank/v1/settings',
430 method: 'POST',
431 data: {
432 [`${aiProvider}_api_key`]: '', // Clear the API key
433 },
434 });
435
436 if (response.success) {
437 setNotice({
438 type: 'success',
439 message: __('API key cleared successfully.', 'thinkrank')
440 });
441 setHasExistingKey(false);
442 setApiKey('');
443
444 // Refresh the settings to ensure UI is in sync
445 await loadSettings();
446 } else {
447 setNotice({
448 type: 'error',
449 message: response.message || __('Failed to clear API key.', 'thinkrank')
450 });
451 }
452 } catch (error) {
453 console.error('Failed to clear API key:', error);
454 setNotice({
455 type: 'error',
456 message: __('Failed to clear API key. Please try again.', 'thinkrank')
457 });
458 } finally {
459 setIsSaving(false);
460 }
461 };
462
463 if (isLoading) {
464 return (
465 <div className="thinkrank-ui thinkrank-max-w-4xl">
466 <div className="thinkrank-flex-center thinkrank-p-8">
467 <div className="thinkrank-text-center">
468 <Spinner />
469 <p className="thinkrank-mt-4 thinkrank-text-secondary">{__('Loading settings...', 'thinkrank')}</p>
470 </div>
471 </div>
472 </div>
473 );
474 }
475
476 return (
477 <div className="thinkrank-ui thinkrank-max-w-4xl">
478 {notice && (
479 <div className="thinkrank-mb-6">
480 <Notice
481 status={notice.type}
482 isDismissible
483 onRemove={() => setNotice(null)}
484 >
485 {notice.message}
486 </Notice>
487 </div>
488 )}
489
490 <div className="thinkrank-mb-8">
491 <h1 className="thinkrank-text-2xl thinkrank-font-bold thinkrank-text-primary thinkrank-mb-2">
492 {__('ThinkRank Settings', 'thinkrank')}
493 </h1>
494 <p className="thinkrank-text-secondary thinkrank-text-base">
495 {__('Configure your AI provider and API settings to start generating content briefs.', 'thinkrank')}
496 </p>
497 </div>
498
499 <div className="thinkrank-flex thinkrank-flex-col thinkrank-gap-6">
500 <div className="thinkrank-card thinkrank-card--elevated">
501 <div className="thinkrank-card__header">
502 <h2 className="thinkrank-text-xl thinkrank-font-semibold thinkrank-text-primary">
503 {__('AI Settings', 'thinkrank')}
504 </h2>
505 </div>
506 <div className="thinkrank-card__body">
507 {/* AI Provider Visual Selector */}
508 <div className="thinkrank-mb-6">
509 <label className="thinkrank-block thinkrank-text-sm thinkrank-font-semibold thinkrank-text-primary thinkrank-mb-2">
510 {__('AI Provider', 'thinkrank')}
511 </label>
512 <p className="thinkrank-text-sm thinkrank-text-secondary thinkrank-mb-4">
513 {__('Choose your preferred AI provider for content generation.', 'thinkrank')}
514 </p>
515
516 <div className="thinkrank-grid thinkrank-grid-cols-1-xs thinkrank-grid-cols-3-sm thinkrank-gap-4">
517 <button
518 type="button"
519 className={`thinkrank-card thinkrank-card--interactive thinkrank-relative thinkrank-p-4 thinkrank-border-2 thinkrank-transition-all ${
520 aiProvider === 'openai'
521 ? 'thinkrank-border-blue thinkrank-bg-blue thinkrank-bg-opacity-5'
522 : 'thinkrank-border-light hover:thinkrank-border-blue'
523 }`}
524 onClick={() => setAiProvider('openai')}
525 >
526 <div className="thinkrank-flex thinkrank-items-center thinkrank-gap-3">
527 <div className={`thinkrank-w-10 thinkrank-h-10 thinkrank-rounded-lg thinkrank-flex-center ${
528 aiProvider === 'openai'
529 ? 'thinkrank-bg-green thinkrank-text-white'
530 : 'thinkrank-bg-gray-100 thinkrank-text-gray-600'
531 }`}>
532 <OpenAIIcon />
533 </div>
534 <div className="thinkrank-flex-1 thinkrank-text-left">
535 <h4 className="thinkrank-text-base thinkrank-font-semibold thinkrank-text-primary thinkrank-mb-1">{__('OpenAI', 'thinkrank')}</h4>
536 <span className="thinkrank-text-sm thinkrank-text-secondary">
537 {aiProvider === 'openai' ? (
538 <ModelBadge provider="openai" model={openaiModelState || settings?.openai_model || 'gpt-5-nano'} />
539 ) : (
540 __('OpenAI API', 'thinkrank')
541 )}
542 </span>
543 </div>
544 {(aiProvider === 'openai' && hasExistingKey) && (
545 <div className="thinkrank-w-5 thinkrank-h-5 thinkrank-text-green">
546 <span className="dashicons dashicons-yes-alt"></span>
547 </div>
548 )}
549 </div>
550 </button>
551
552 <button
553 type="button"
554 className={`thinkrank-card thinkrank-card--interactive thinkrank-relative thinkrank-p-4 thinkrank-border-2 thinkrank-transition-all ${
555 aiProvider === 'claude'
556 ? 'thinkrank-border-orange thinkrank-bg-orange thinkrank-bg-opacity-5'
557 : 'thinkrank-border-light hover:thinkrank-border-orange'
558 }`}
559 onClick={() => setAiProvider('claude')}
560 >
561 <div className="thinkrank-flex thinkrank-items-center thinkrank-gap-3">
562 <div className={`thinkrank-w-10 thinkrank-h-10 thinkrank-rounded-lg thinkrank-flex-center ${
563 aiProvider === 'claude'
564 ? 'thinkrank-bg-orange thinkrank-text-white'
565 : 'thinkrank-bg-gray-100 thinkrank-text-gray-600'
566 }`}>
567 <ClaudeIcon />
568 </div>
569 <div className="thinkrank-flex-1 thinkrank-text-left">
570 <h4 className="thinkrank-text-base thinkrank-font-semibold thinkrank-text-primary thinkrank-mb-1">{__('Claude', 'thinkrank')}</h4>
571 <span className="thinkrank-text-sm thinkrank-text-secondary">
572 {aiProvider === 'claude' ? (
573 <ModelBadge provider="claude" model={claudeModelState || settings?.claude_model || 'claude-3-7-sonnet-latest'} />
574 ) : (
575 __('Anthropic', 'thinkrank')
576 )}
577 </span>
578 </div>
579 {(aiProvider === 'claude' && hasExistingKey) && (
580 <div className="thinkrank-w-5 thinkrank-h-5 thinkrank-text-green">
581 <span className="dashicons dashicons-yes-alt"></span>
582 </div>
583 )}
584 </div>
585 </button>
586
587 <button
588 type="button"
589 className={`thinkrank-card thinkrank-card--interactive thinkrank-relative thinkrank-p-4 thinkrank-border-2 thinkrank-transition-all ${
590 aiProvider === 'gemini'
591 ? 'thinkrank-border-purple thinkrank-bg-purple thinkrank-bg-opacity-5'
592 : 'thinkrank-border-light hover:thinkrank-border-purple'
593 }`}
594 onClick={() => setAiProvider('gemini')}
595 >
596 <div className="thinkrank-flex thinkrank-items-center thinkrank-gap-3">
597 <div className={`thinkrank-w-10 thinkrank-h-10 thinkrank-rounded-lg thinkrank-flex-center ${
598 aiProvider === 'gemini'
599 ? 'thinkrank-bg-purple thinkrank-text-white'
600 : 'thinkrank-bg-gray-100 thinkrank-text-gray-600'
601 }`}>
602 <GeminiIcon />
603 </div>
604 <div className="thinkrank-flex-1 thinkrank-text-left">
605 <h4 className="thinkrank-text-base thinkrank-font-semibold thinkrank-text-primary thinkrank-mb-1">{__('Gemini', 'thinkrank')}</h4>
606 <span className="thinkrank-text-sm thinkrank-text-secondary">
607 {aiProvider === 'gemini' ? (
608 <ModelBadge provider="gemini" model={geminiModelState || settings?.gemini_model || 'gemini-2.5-flash'} />
609 ) : (
610 __('Google AI', 'thinkrank')
611 )}
612 </span>
613 </div>
614 {(aiProvider === 'gemini' && hasExistingKey) && (
615 <div className="thinkrank-w-5 thinkrank-h-5 thinkrank-text-green">
616 <span className="dashicons dashicons-yes-alt"></span>
617 </div>
618 )}
619 </div>
620 </button>
621 </div>
622 </div>
623
624 {/* API Key Input */}
625 <div className="thinkrank-mb-6">
626 <div className="thinkrank-mb-4">
627 <TextControl
628 label={__(`${aiProvider === 'openai' ? 'OpenAI' : aiProvider === 'claude' ? 'Claude' : 'Gemini'} API Key`, 'thinkrank')}
629 type="password"
630 value={apiKey}
631 onChange={setApiKey}
632 help={hasExistingKey
633 ? __('API key is saved. Enter a new key to update it.', 'thinkrank')
634 : __('Enter your API key. This will be encrypted and stored securely.', 'thinkrank')
635 }
636 placeholder={hasExistingKey
637 ? __('Enter new API key to update...', 'thinkrank')
638 : (aiProvider === 'openai' ? __('sk-...', 'thinkrank') : aiProvider === 'claude' ? __('sk-ant-...', 'thinkrank') : __('AIza...', 'thinkrank'))
639 }
640 __next40pxDefaultSize={true}
641 __nextHasNoMarginBottom={true}
642 />
643 </div>
644
645 {/* Model Selection - Provider Specific */}
646 <div className="thinkrank-mb-4">
647 {aiProvider === 'openai' && (
648 <SelectControl
649 label={__('OpenAI Model', 'thinkrank')}
650 value={openaiModelState}
651 options={modelOptions.openai}
652 onChange={(v) => { setOpenaiModelState(v); setSelectedModel(v); }}
653 help={__('Choose the OpenAI model to use for content generation.', 'thinkrank')}
654 __next40pxDefaultSize={true}
655 __nextHasNoMarginBottom={true}
656 />
657 )}
658
659 {aiProvider === 'claude' && (
660 <SelectControl
661 label={__('Claude Model', 'thinkrank')}
662 value={claudeModelState}
663 options={modelOptions.claude}
664 onChange={(v) => { setClaudeModelState(v); setSelectedModel(v); }}
665 help={__('Choose the Claude model to use for content generation.', 'thinkrank')}
666 __next40pxDefaultSize={true}
667 __nextHasNoMarginBottom={true}
668 />
669 )}
670
671 {aiProvider === 'gemini' && (
672 <SelectControl
673 label={__('Gemini Model', 'thinkrank')}
674 value={geminiModelState}
675 options={modelOptions.gemini}
676 onChange={(v) => { setGeminiModelState(v); setSelectedModel(v); }}
677 help={__('Choose the Gemini model to use for content generation.', 'thinkrank')}
678 __next40pxDefaultSize={true}
679 __nextHasNoMarginBottom={true}
680 />
681 )}
682 </div>
683
684 {hasExistingKey && (
685 <div className="thinkrank-mt-4 thinkrank-p-4 thinkrank-bg-green thinkrank-bg-opacity-10 thinkrank-border thinkrank-border-green thinkrank-border-opacity-20 thinkrank-rounded-lg">
686 <div className="thinkrank-flex thinkrank-items-center thinkrank-gap-2 thinkrank-mb-2">
687 <span className="dashicons dashicons-yes-alt thinkrank-text-green"></span>
688 <span className="thinkrank-text-sm thinkrank-font-medium thinkrank-text-green">
689 {__('API key is configured and saved.', 'thinkrank')}
690 </span>
691 </div>
692 <div className="thinkrank-flex thinkrank-items-start thinkrank-gap-2">
693 <span className="dashicons dashicons-info thinkrank-text-blue thinkrank-mt-1"></span>
694 <span className="thinkrank-text-sm thinkrank-text-secondary thinkrank-leading-relaxed">
695 {__('With your own API key, you have unlimited AI generations at no additional cost. You pay the provider directly.', 'thinkrank')}
696 </span>
697 </div>
698 </div>
699 )}
700 </div>
701
702 {/* Action Buttons */}
703 <div className="thinkrank-flex thinkrank-flex-col-xs thinkrank-flex-row-sm thinkrank-gap-3 thinkrank-pt-6 thinkrank-border-t thinkrank-border-light">
704 <button
705 className={`thinkrank-btn thinkrank-btn--primary thinkrank-btn--base ${isSaving ? 'thinkrank-btn--loading' : ''}`}
706 onClick={handleSave}
707 disabled={isTesting || isSaving}
708 >
709 {isSaving ? __('Saving...', 'thinkrank') : __('Save Settings', 'thinkrank')}
710 </button>
711
712 <button
713 className={`thinkrank-btn thinkrank-btn--secondary thinkrank-btn--base ${isTesting ? 'thinkrank-btn--loading' : ''}`}
714 onClick={handleTestConnection}
715 disabled={isSaving || !apiKey.trim() || isTesting}
716 >
717 {isTesting ? __('Testing...', 'thinkrank') : __('Test Connection', 'thinkrank')}
718 </button>
719
720 {hasExistingKey && (
721 <button
722 className="thinkrank-btn thinkrank-btn--danger thinkrank-btn--base"
723 onClick={handleClearApiKey}
724 disabled={isTesting || isSaving}
725 >
726 {__('Clear API Key', 'thinkrank')}
727 </button>
728 )}
729 </div>
730 </div>
731 </div>
732
733 <div className="thinkrank-card thinkrank-card--elevated">
734 <div className="thinkrank-card__header">
735 <h3 className="thinkrank-text-xl thinkrank-font-semibold thinkrank-text-primary">
736 {__('Data Management', 'thinkrank')}
737 </h3>
738 </div>
739 <div className="thinkrank-card__body">
740 <div className="thinkrank-mb-6">
741 <CheckboxControl
742 label={__('Delete all data when uninstalling plugin', 'thinkrank')}
743 help={__('By default, your API keys and content briefs are preserved when you uninstall ThinkRank. Check this box to delete everything instead.', 'thinkrank')}
744 checked={deleteDataOnUninstall}
745 onChange={setDeleteDataOnUninstall}
746 __nextHasNoMarginBottom={true}
747 />
748 </div>
749
750 <div className="thinkrank-p-5 thinkrank-bg-blue thinkrank-bg-opacity-5 thinkrank-border thinkrank-border-blue thinkrank-border-opacity-20 thinkrank-rounded-lg thinkrank-border-l-4 thinkrank-border-l-blue">
751 <div className="thinkrank-grid thinkrank-grid-cols-1-xs thinkrank-grid-cols-2-md thinkrank-gap-6">
752 <div>
753 <h4 className="thinkrank-flex thinkrank-items-center thinkrank-gap-2 thinkrank-text-sm thinkrank-font-semibold thinkrank-text-blue thinkrank-mb-3">
754 <span className="thinkrank-text-green"></span>
755 {__('What gets preserved', 'thinkrank')}
756 </h4>
757 <ul className="thinkrank-text-sm thinkrank-text-secondary thinkrank-leading-relaxed thinkrank-pl-4 thinkrank-space-y-1">
758 <li>{__('API keys and provider settings', 'thinkrank')}</li>
759 <li>{__('Generated content briefs', 'thinkrank')}</li>
760 <li>{__('Plugin configuration', 'thinkrank')}</li>
761 </ul>
762 </div>
763
764 <div>
765 <h4 className="thinkrank-flex thinkrank-items-center thinkrank-gap-2 thinkrank-text-sm thinkrank-font-semibold thinkrank-text-blue thinkrank-mb-3">
766 <span className="thinkrank-text-red"></span>
767 {__('What gets cleaned up', 'thinkrank')}
768 </h4>
769 <ul className="thinkrank-text-sm thinkrank-text-secondary thinkrank-leading-relaxed thinkrank-pl-4 thinkrank-space-y-1">
770 <li>{__('Temporary cache data', 'thinkrank')}</li>
771 <li>{__('Usage analytics', 'thinkrank')}</li>
772 <li>{__('Plugin activation flags', 'thinkrank')}</li>
773 </ul>
774 </div>
775 </div>
776 </div>
777 </div>
778 </div>
779 </div>
780 </div>
781 );
782 };
783
784 export default Settings;
785