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 / content-brief / ContentBriefGenerator.js

ContentBriefGenerator.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/content-brief/ContentBriefGenerator.js

766 lines 37.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Content Brief Generator Component
3 *
4 * Main interface for generating AI-powered content briefs
5 *
6 * @package ThinkRank
7 * @since 1.0.0
8 */
9
10 import { useState, useEffect } from '@wordpress/element';
11 import { TextControl, SelectControl, TextareaControl, Notice, Spinner } from '@wordpress/components';
12 import { __ } from '@wordpress/i18n';
13 import apiFetch from '@wordpress/api-fetch';
14
15 import KeywordInput from './KeywordInput';
16 import ContentStructure from './ContentStructure';
17 import ExportOptions from './ExportOptions';
18 import ModelBadge from '../shared/ModelBadge';
19
20
21
22 const ContentBriefGenerator = () => {
23 // Form state
24 const [formData, setFormData] = useState({
25 target_keywords: [''],
26 content_type: 'blog_post',
27 target_audience: 'general',
28 content_length: 'medium',
29 tone: 'professional',
30 competitor_urls: [''],
31 additional_context: ''
32 });
33
34 // UI state
35 const [isGenerating, setIsGenerating] = useState(false);
36 const [generatedBrief, setGeneratedBrief] = useState(null);
37 const [error, setError] = useState(null);
38 const [savedBriefs, setSavedBriefs] = useState([]);
39
40 // Content type options
41 const contentTypeOptions = [
42 { label: __('Blog Post', 'thinkrank'), value: 'blog_post' },
43 { label: __('Product Page', 'thinkrank'), value: 'product_page' },
44 { label: __('Landing Page', 'thinkrank'), value: 'landing_page' },
45 { label: __('Tutorial', 'thinkrank'), value: 'tutorial' }
46 ];
47
48 // Target audience options
49 const audienceOptions = [
50 { label: __('General Audience', 'thinkrank'), value: 'general' },
51 { label: __('Beginners', 'thinkrank'), value: 'beginners' },
52 { label: __('Professionals', 'thinkrank'), value: 'professionals' },
53 { label: __('Experts', 'thinkrank'), value: 'experts' }
54 ];
55
56 // Content length options
57 const lengthOptions = [
58 { label: __('Short (500-800 words)', 'thinkrank'), value: 'short' },
59 { label: __('Medium (1000-1500 words)', 'thinkrank'), value: 'medium' },
60 { label: __('Long (2000+ words)', 'thinkrank'), value: 'long' }
61 ];
62
63 // Tone options
64 const toneOptions = [
65 { label: __('Professional', 'thinkrank'), value: 'professional' },
66 { label: __('Casual', 'thinkrank'), value: 'casual' },
67 { label: __('Technical', 'thinkrank'), value: 'technical' },
68 { label: __('Friendly', 'thinkrank'), value: 'friendly' }
69 ];
70
71 // Load saved briefs on component mount
72 useEffect(() => {
73 loadSavedBriefs();
74 }, []);
75
76 /**
77 * Load saved briefs from API
78 */
79 const loadSavedBriefs = async () => {
80 try {
81 const response = await apiFetch({
82 path: '/thinkrank/v1/content-brief/list',
83 method: 'GET'
84 });
85
86 if (response.success) {
87 setSavedBriefs(response.data);
88 }
89 } catch (error) {
90 console.error('Failed to load saved briefs:', error);
91 }
92 };
93
94 /**
95 * Handle form field changes
96 */
97 const handleFieldChange = (field, value) => {
98 setFormData(prev => ({
99 ...prev,
100 [field]: value
101 }));
102 };
103
104 /**
105 * Handle keyword changes
106 */
107 const handleKeywordsChange = (keywords) => {
108 setFormData(prev => ({
109 ...prev,
110 target_keywords: keywords
111 }));
112 };
113
114 /**
115 * Handle competitor URL changes
116 */
117 const handleCompetitorUrlsChange = (urls) => {
118 setFormData(prev => ({
119 ...prev,
120 competitor_urls: urls
121 }));
122 };
123
124 /**
125 * Validate form data
126 */
127 const validateForm = () => {
128 const errors = [];
129
130 // Check if at least one keyword is provided
131 const validKeywords = formData.target_keywords.filter(keyword => keyword.trim() !== '');
132 if (validKeywords.length === 0) {
133 errors.push(__('At least one target keyword is required.', 'thinkrank'));
134 }
135
136 // Validate competitor URLs if provided
137 const validUrls = formData.competitor_urls.filter(url => url.trim() !== '');
138 for (const url of validUrls) {
139 try {
140 new URL(url);
141 } catch {
142 errors.push(__(`Invalid URL: ${url}`, 'thinkrank'));
143 }
144 }
145
146 return errors;
147 };
148
149 /**
150 * Generate content brief
151 */
152 const generateBrief = async () => {
153 setError(null);
154
155 // Validate form
156 const validationErrors = validateForm();
157 if (validationErrors.length > 0) {
158 setError(validationErrors.join(' '));
159 return;
160 }
161
162 setIsGenerating(true);
163
164 try {
165 // Clean up form data
166 const cleanedData = {
167 ...formData,
168 target_keywords: formData.target_keywords.filter(keyword => keyword.trim() !== ''),
169 competitor_urls: formData.competitor_urls.filter(url => url.trim() !== '')
170 };
171
172 const response = await apiFetch({
173 path: '/thinkrank/v1/content-brief/generate',
174 method: 'POST',
175 data: cleanedData
176 });
177
178 if (response.success) {
179 setGeneratedBrief(response.data);
180 loadSavedBriefs(); // Refresh saved briefs list
181
182 // Scroll to the generated content area after a brief delay to ensure content is rendered
183 setTimeout(() => {
184 const contentSection = document.getElementById('generated-content-section');
185 if (contentSection) {
186 contentSection.scrollIntoView({
187 behavior: 'smooth',
188 block: 'start'
189 });
190 }
191 }, 200);
192 } else {
193 setError(response.message || __('Failed to generate content brief.', 'thinkrank'));
194 }
195 } catch (error) {
196 // Handle API configuration errors gracefully without console spam
197 if (error.message && error.message.includes('Please configure your AI provider')) {
198 setError(error.message);
199 } else {
200 // Only log unexpected errors to console
201 console.error('Brief generation error:', error);
202 setError(error.message || __('An error occurred while generating the content brief.', 'thinkrank'));
203 }
204 } finally {
205 setIsGenerating(false);
206 }
207 };
208
209
210
211 /**
212 * Delete a saved brief
213 */
214 const deleteSavedBrief = async (briefId) => {
215 if (!confirm(__('Are you sure you want to delete this brief? This action cannot be undone.', 'thinkrank'))) {
216 return;
217 }
218
219 try {
220 const response = await apiFetch({
221 path: `/thinkrank/v1/content-brief/${briefId}`,
222 method: 'DELETE'
223 });
224
225 if (response.success) {
226 // Remove the brief from the local state
227 setSavedBriefs(savedBriefs.filter(brief => brief.id !== briefId));
228
229 // If the deleted brief is currently displayed, clear it
230 if (generatedBrief && generatedBrief.id === briefId) {
231 setGeneratedBrief(null);
232 }
233 }
234 } catch (error) {
235 console.error('Failed to delete brief:', error);
236 alert(__('Failed to delete brief. Please try again.', 'thinkrank'));
237 }
238 };
239
240 /**
241 * Clear current brief
242 */
243 const clearBrief = () => {
244 setGeneratedBrief(null);
245 setError(null);
246 };
247
248 /**
249 * Copy text to clipboard with fallback (same as ContentStructure.js)
250 */
251 const copyToClipboard = async (text) => {
252 try {
253 // Try modern clipboard API first
254 if (navigator.clipboard && navigator.clipboard.writeText) {
255 await navigator.clipboard.writeText(text);
256 console.log('Copied to clipboard');
257 return;
258 }
259
260 // Fallback for older browsers or non-HTTPS contexts
261 const textArea = document.createElement('textarea');
262 textArea.value = text;
263 textArea.style.position = 'fixed';
264 textArea.style.left = '-999999px';
265 textArea.style.top = '-999999px';
266 document.body.appendChild(textArea);
267 textArea.focus();
268 textArea.select();
269
270 try {
271 document.execCommand('copy');
272 console.log('Copied to clipboard (fallback)');
273 } catch (err) {
274 console.error('Failed to copy text: ', err);
275 }
276
277 document.body.removeChild(textArea);
278 } catch (err) {
279 console.error('Copy to clipboard failed: ', err);
280 }
281 };
282
283 /**
284 * Format brief data as plain text for export
285 */
286 const formatBriefAsText = (data) => {
287 let text = '';
288
289 // Header
290 if (data.title && Array.isArray(data.title) && data.title[0]) {
291 text += `Content Brief: "${data.title[0]}"\n`;
292 text += '='.repeat(60) + '\n\n';
293 } else if (data.title && typeof data.title === 'string') {
294 text += `Content Brief: "${data.title}"\n`;
295 text += '='.repeat(60) + '\n\n';
296 }
297
298 // Generation parameters
299 if (data.generation_params) {
300 const params = data.generation_params;
301 text += 'BRIEF CONFIGURATION\n';
302 text += `-`.repeat(30) + '\n';
303 text += `Target Keywords: ${params.target_keywords?.join(', ') || 'N/A'}\n`;
304 text += `Content Type: ${params.content_type || 'N/A'}\n`;
305 text += `Target Audience: ${params.target_audience || 'N/A'}\n`;
306 text += `Content Length: ${params.content_length || 'N/A'}\n`;
307 text += `Tone: ${params.tone || 'N/A'}\n`;
308 if (params.competitor_urls && params.competitor_urls.length > 0) {
309 text += `Competitor URLs: ${params.competitor_urls.join(', ')}\n`;
310 }
311 if (params.additional_context) {
312 text += `Additional Context: ${params.additional_context}\n`;
313 }
314 text += '\n';
315 }
316
317 // Title suggestions
318 if (data.title && Array.isArray(data.title) && data.title.length > 1) {
319 text += 'TITLE SUGGESTIONS\n';
320 text += `-`.repeat(30) + '\n';
321 data.title.forEach((title, index) => {
322 text += `${index + 1}. ${title}\n`;
323 });
324 text += '\n';
325 }
326
327 // Content outline
328 if (data.outline && Array.isArray(data.outline) && data.outline.length > 0) {
329 text += 'CONTENT OUTLINE\n';
330 text += `-`.repeat(30) + '\n';
331 data.outline.forEach((item, index) => {
332 const indent = ' '.repeat((item.level || 1) - 1);
333 text += `${indent}${index + 1}. ${item.heading || item}`;
334 if (item.word_count && item.word_count > 0) {
335 text += ` (${item.word_count} words)`;
336 }
337 text += '\n';
338
339 if (item.key_points && Array.isArray(item.key_points) && item.key_points.length > 0) {
340 item.key_points.forEach((point) => {
341 text += `${indent}${point}\n`;
342 });
343 }
344 text += '\n';
345 });
346 }
347
348 // SEO recommendations
349 if (data.seo_recommendations) {
350 text += 'SEO RECOMMENDATIONS\n';
351 text += `-`.repeat(30) + '\n';
352
353 const seo = data.seo_recommendations;
354 if (seo.related_keywords && seo.related_keywords.length > 0) {
355 text += `Related Keywords: ${seo.related_keywords.join(', ')}\n`;
356 }
357 if (seo.internal_links && seo.internal_links.length > 0) {
358 text += 'Internal Linking Opportunities:\n';
359 seo.internal_links.forEach((link) => {
360 text += `${link}\n`;
361 });
362 }
363 text += '\n';
364 }
365
366 // Call-to-actions
367 if (data.call_to_actions && data.call_to_actions.length > 0) {
368 text += 'CALL-TO-ACTION SUGGESTIONS\n';
369 text += `-`.repeat(30) + '\n';
370 data.call_to_actions.forEach((cta, index) => {
371 text += `${index + 1}. ${cta}\n`;
372 });
373 text += '\n';
374 }
375
376 // Footer
377 text += '='.repeat(60) + '\n';
378 if (data.created_at) {
379 text += `Generated on: ${data.created_at}\n`;
380 }
381 text += 'Powered by ThinkRank\n';
382
383 return text;
384 };
385
386 /**
387 * Load a saved brief
388 */
389 const loadSavedBrief = (brief) => {
390 // Load the brief data into the form
391 if (brief.brief_data) {
392 setFormData({
393 target_keywords: brief.brief_data.target_keywords || [''],
394 content_type: brief.brief_data.content_type || 'blog_post',
395 target_audience: brief.brief_data.target_audience || 'general',
396 content_length: brief.brief_data.content_length || 'medium',
397 tone: brief.brief_data.tone || 'professional',
398 competitor_urls: brief.brief_data.competitor_urls || [''],
399 additional_context: brief.brief_data.additional_context || ''
400 });
401 }
402
403 // Set the generated brief
404 setGeneratedBrief(brief.brief_data);
405 setError(null);
406
407 // Scroll to the generated content area after a brief delay to ensure content is rendered
408 setTimeout(() => {
409 const contentSection = document.getElementById('generated-content-section');
410 if (contentSection) {
411 contentSection.scrollIntoView({
412 behavior: 'smooth',
413 block: 'start'
414 });
415 }
416 }, 100);
417 };
418
419
420
421 return (
422 <div className="thinkrank-ui thinkrank-max-w-7xl thinkrank-mx-auto thinkrank-p-5 thinkrank-px-4-xs thinkrank-px-6-md">
423 {/* Error Notice */}
424 {error && (
425 <div className="thinkrank-mb-6">
426 <Notice status="error" isDismissible onRemove={() => setError(null)}>
427 {error}
428 </Notice>
429 </div>
430 )}
431
432 {/* Section 1: Form + Saved Briefs */}
433 <div className="thinkrank-grid thinkrank-grid-cols-1-xs thinkrank-grid-cols-5-lg thinkrank-gap-6 thinkrank-mb-8">
434 {/* Left Column - AI-Inspired Form (3/5 width) */}
435 <div className="thinkrank-col-span-3-lg">
436 {/* AI-Inspired Brief Configuration Card */}
437 <div className="thinkrank-ai-card">
438 {/* Header with AI Icon */}
439 <div className="thinkrank-ai-card__header">
440 <div className="thinkrank-ai-icon">
441 <span className="thinkrank-sparkles"></span>
442 </div>
443 <div className="thinkrank-ai-card__title">
444 <h3>{__('Create Content Brief', 'thinkrank')}</h3>
445 <p>{__('AI-powered content strategy and optimization', 'thinkrank')}</p>
446 </div>
447 </div>
448
449 {/* Form Content */}
450 <div className="thinkrank-ai-card__body">
451 {/* Target Keywords */}
452 <div className="thinkrank-mb-6">
453 <KeywordInput
454 keywords={formData.target_keywords}
455 onChange={handleKeywordsChange}
456 label={__('Target Keywords', 'thinkrank')}
457 help={__('Enter your primary and secondary keywords', 'thinkrank')}
458 />
459 </div>
460
461 {/* Content Configuration - Two Column Grid */}
462 <div className="thinkrank-grid thinkrank-grid-cols-1-xs thinkrank-grid-cols-2-sm thinkrank-gap-4 thinkrank-mb-6">
463 <SelectControl
464 label={__('Content Type', 'thinkrank')}
465 value={formData.content_type}
466 options={contentTypeOptions}
467 onChange={(value) => handleFieldChange('content_type', value)}
468 __next40pxDefaultSize={true}
469 __nextHasNoMarginBottom={true}
470 />
471 <SelectControl
472 label={__('Target Audience', 'thinkrank')}
473 value={formData.target_audience}
474 options={audienceOptions}
475 onChange={(value) => handleFieldChange('target_audience', value)}
476 __next40pxDefaultSize={true}
477 __nextHasNoMarginBottom={true}
478 />
479 </div>
480
481 <div className="thinkrank-grid thinkrank-grid-cols-1-xs thinkrank-grid-cols-2-sm thinkrank-gap-4 thinkrank-mb-6">
482 <SelectControl
483 label={__('Content Length', 'thinkrank')}
484 value={formData.content_length}
485 options={lengthOptions}
486 onChange={(value) => handleFieldChange('content_length', value)}
487 __next40pxDefaultSize={true}
488 __nextHasNoMarginBottom={true}
489 />
490 <SelectControl
491 label={__('Tone', 'thinkrank')}
492 value={formData.tone}
493 options={toneOptions}
494 onChange={(value) => handleFieldChange('tone', value)}
495 __next40pxDefaultSize={true}
496 __nextHasNoMarginBottom={true}
497 />
498 </div>
499
500 {/* Advanced Options Section */}
501 <div className="thinkrank-ai-section">
502 <h4 className="thinkrank-ai-section__title">
503 {__('Advanced Options', 'thinkrank')}
504 </h4>
505
506 {/* Competitor URLs */}
507 <div className="thinkrank-mb-4">
508 <KeywordInput
509 keywords={formData.competitor_urls}
510 onChange={handleCompetitorUrlsChange}
511 label={__('Competitor URLs (Optional)', 'thinkrank')}
512 help={__('Add competitor URLs for comprehensive analysis and content gap identification. Note: Analysis may take 30-60 seconds with competitor URLs.', 'thinkrank')}
513 placeholder={__('https://example.com/competitor-article', 'thinkrank')}
514 />
515 </div>
516
517 {/* Additional Context */}
518 <TextareaControl
519 label={__('Additional Context', 'thinkrank')}
520 value={formData.additional_context}
521 onChange={(value) => handleFieldChange('additional_context', value)}
522 placeholder={__('Any specific requirements, brand guidelines, or context for the content...', 'thinkrank')}
523 rows={3}
524 __nextHasNoMarginBottom={true}
525 />
526 </div>
527 </div>
528
529 {/* AI Generate Button */}
530 <div className="thinkrank-ai-card__footer">
531 <button
532 className={`thinkrank-ai-btn ${isGenerating ? 'thinkrank-ai-btn--loading' : ''}`}
533 onClick={generateBrief}
534 disabled={isGenerating}
535 >
536 {isGenerating ? (
537 <>
538 <Spinner />
539 <span className="thinkrank-ml-2">
540 {formData.competitor_urls.filter(url => url.trim()).length > 0
541 ? __('Analyzing competitors & generating...', 'thinkrank')
542 : __('Generating Brief...', 'thinkrank')
543 }
544 </span>
545 </>
546 ) : (
547 <>
548 <span className="thinkrank-sparkles thinkrank-mr-2"></span>
549 {__('Generate Content Brief', 'thinkrank')}
550 </>
551 )}
552 </button>
553 </div>
554 </div>
555 </div>
556
557 {/* Right Column - Saved Briefs (2/5 width) */}
558 <div className="thinkrank-col-span-2-lg">
559 <div className="thinkrank-card thinkrank-card--elevated">
560 <div className="thinkrank-card__header">
561 <h3 className="thinkrank-text-lg thinkrank-font-semibold thinkrank-text-primary">
562 {__('Saved Briefs', 'thinkrank')}
563 </h3>
564 </div>
565 <div className="thinkrank-card__body thinkrank-max-h-160 thinkrank-overflow-y-auto">
566 {savedBriefs.length > 0 ? (
567 <div className="thinkrank-space-y-2">
568 {savedBriefs.map((brief) => (
569 <div key={brief.id} className="thinkrank-saved-brief-card thinkrank-bg-gray-50 thinkrank-border thinkrank-border-medium thinkrank-rounded-lg thinkrank-p-4 thinkrank-shadow-md">
570 <div className="thinkrank-flex thinkrank-items-start thinkrank-justify-between thinkrank-mb-2">
571 <div className="thinkrank-flex-1 thinkrank-min-w-0">
572 <h4 className="thinkrank-text-sm thinkrank-font-semibold thinkrank-text-primary thinkrank-truncate thinkrank-leading-tight thinkrank-m-0">
573 {brief.title || __('Untitled Brief', 'thinkrank')}
574 </h4>
575 <p className="thinkrank-text-xs thinkrank-text-secondary thinkrank-italic thinkrank-mt-1 thinkrank-m-0">
576 {new Date(brief.created_at).toLocaleDateString()}
577 </p>
578 </div>
579 {brief.brief_data?.generation_meta && (
580 <ModelBadge
581 provider={brief.brief_data.generation_meta.provider}
582 model={brief.brief_data.generation_meta.model}
583 />
584 )}
585 </div>
586
587 {brief.target_keywords && brief.target_keywords.length > 0 && (
588 <div className="thinkrank-mb-3">
589 <div className="thinkrank-flex thinkrank-flex-wrap thinkrank-gap-1">
590 {brief.target_keywords.slice(0, 2).map((keyword, index) => (
591 <span key={index} className="thinkrank-px-2 thinkrank-py-0.5 thinkrank-bg-blue thinkrank-bg-opacity-10 thinkrank-text-blue thinkrank-text-xs thinkrank-rounded-full">
592 {keyword}
593 </span>
594 ))}
595 {brief.target_keywords.length > 2 && (
596 <span className="thinkrank-px-2 thinkrank-py-0.5 thinkrank-bg-gray-200 thinkrank-text-gray-600 thinkrank-text-xs thinkrank-rounded-full">
597 +{brief.target_keywords.length - 2}
598 </span>
599 )}
600 </div>
601 </div>
602 )}
603
604 <div className="thinkrank-flex thinkrank-gap-2">
605 <button
606 className="thinkrank-btn thinkrank-btn--secondary thinkrank-btn--sm"
607 style={{ maxWidth: '100px' }}
608 onClick={() => loadSavedBrief(brief)}
609 >
610 {__('View Brief', 'thinkrank')}
611 </button>
612 <button
613 className="thinkrank-btn thinkrank-btn--danger thinkrank-btn--sm thinkrank-w-8 thinkrank-h-8 thinkrank-flex-center"
614 onClick={() => deleteSavedBrief(brief.id)}
615 title={__('Delete brief', 'thinkrank')}
616 >
617 <svg className="thinkrank-w-4 thinkrank-h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
618 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
619 </svg>
620 </button>
621 </div>
622 </div>
623 ))}
624 </div>
625 ) : (
626 <div className="thinkrank-text-center thinkrank-py-8">
627 <div className="thinkrank-w-16 thinkrank-h-16 thinkrank-mx-auto thinkrank-mb-4 thinkrank-bg-gray-100 thinkrank-rounded-full thinkrank-flex-center">
628 <svg className="thinkrank-w-8 thinkrank-h-8 thinkrank-text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
629 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
630 </svg>
631 </div>
632 <p className="thinkrank-text-sm thinkrank-text-secondary">
633 {__('No saved briefs yet. Generate your first brief to get started!', 'thinkrank')}
634 </p>
635 </div>
636 )}
637 </div>
638 </div>
639 </div>
640 </div>
641
642 {/* Section 2: Full-Width Generated Content */}
643 {generatedBrief && (
644 <div id="generated-content-section" className="thinkrank-card thinkrank-card--elevated">
645 <div className="thinkrank-card__header">
646 <div className="thinkrank-flex thinkrank-items-center thinkrank-justify-between">
647 <div className="thinkrank-flex thinkrank-items-center thinkrank-gap-3">
648 <div className="thinkrank-w-8 thinkrank-h-8 thinkrank-rounded-full thinkrank-bg-purple thinkrank-text-white thinkrank-flex-center thinkrank-text-sm thinkrank-font-semibold">
649 3
650 </div>
651 <h3 className="thinkrank-text-lg thinkrank-font-semibold thinkrank-text-primary">
652 {__('Generated Content Brief', 'thinkrank')}
653 </h3>
654 <ModelBadge provider={generatedBrief.ai_provider} model={generatedBrief.ai_model} />
655 </div>
656 <div className="thinkrank-flex thinkrank-gap-2">
657 <button
658 className="thinkrank-btn thinkrank-btn--primary thinkrank-btn--sm"
659 onClick={() => {
660 const formattedText = formatBriefAsText(generatedBrief);
661 const blob = new Blob([formattedText], { type: 'text/plain' });
662 const url = URL.createObjectURL(blob);
663 const link = document.createElement('a');
664 link.href = url;
665 link.download = `content-brief-${Date.now()}.txt`;
666 document.body.appendChild(link);
667 link.click();
668 document.body.removeChild(link);
669 URL.revokeObjectURL(url);
670 }}
671 >
672 <svg className="thinkrank-w-4 thinkrank-h-4 thinkrank-mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
673 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
674 </svg>
675 {__('Save as Text', 'thinkrank')}
676 </button>
677 <button
678 className="thinkrank-btn thinkrank-btn--secondary thinkrank-btn--sm"
679 onClick={() => {
680 const formattedText = formatBriefAsText(generatedBrief);
681 // Create a new window with formatted content for PDF printing
682 const printWindow = window.open('', '_blank');
683 printWindow.document.write(`
684 <html>
685 <head>
686 <title>Content Brief</title>
687 <style>
688 body {
689 font-family: Arial, sans-serif;
690 margin: 40px;
691 line-height: 1.6;
692 color: #333;
693 }
694 h1 {
695 color: #333;
696 border-bottom: 2px solid #333;
697 padding-bottom: 10px;
698 }
699 h2 {
700 color: #666;
701 margin-top: 30px;
702 border-bottom: 1px solid #ccc;
703 padding-bottom: 5px;
704 }
705 .section { margin-bottom: 20px; }
706 .outline-item { margin-left: 20px; }
707 pre {
708 white-space: pre-wrap;
709 font-family: Arial, sans-serif;
710 font-size: 12px;
711 }
712 @media print {
713 body { margin: 20px; }
714 @page { margin: 1in; }
715 }
716 </style>
717 </head>
718 <body>
719 <pre>${formattedText.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>
720 </body>
721 </html>
722 `);
723 printWindow.document.close();
724 printWindow.print();
725 }}
726 >
727 <svg className="thinkrank-w-4 thinkrank-h-4 thinkrank-mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
728 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
729 </svg>
730 {__('Save as PDF', 'thinkrank')}
731 </button>
732 <button
733 className="thinkrank-btn thinkrank-btn--secondary thinkrank-btn--sm"
734 onClick={() => {
735 const formattedText = formatBriefAsText(generatedBrief);
736 copyToClipboard(formattedText);
737 }}
738 >
739 <svg className="thinkrank-w-4 thinkrank-h-4 thinkrank-mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
740 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
741 </svg>
742 {__('Copy to Clipboard', 'thinkrank')}
743 </button>
744 <button
745 className="thinkrank-btn thinkrank-btn--secondary thinkrank-btn--sm"
746 onClick={clearBrief}
747 >
748 <svg className="thinkrank-w-4 thinkrank-h-4 thinkrank-mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
749 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
750 </svg>
751 {__('Close', 'thinkrank')}
752 </button>
753 </div>
754 </div>
755 </div>
756 <div className="thinkrank-card__body">
757 <ContentStructure briefData={generatedBrief} formData={formData} />
758 </div>
759 </div>
760 )}
761 </div>
762 );
763 };
764
765 export default ContentBriefGenerator;
766