| 1 |
/** |
| 2 |
* Dashboard Page Component |
| 3 |
* |
| 4 |
* Main dashboard for ThinkRank admin interface |
| 5 |
* |
| 6 |
* @package ThinkRank |
| 7 |
* @since 1.0.0 |
| 8 |
*/ |
| 9 |
|
| 10 |
import { useState, useEffect } from '@wordpress/element'; |
| 11 |
import { __ } from '@wordpress/i18n'; |
| 12 |
import { Card, CardBody, CardHeader, Spinner } from '@wordpress/components'; |
| 13 |
import apiFetch from '@wordpress/api-fetch'; |
| 14 |
|
| 15 |
// AI Provider Icons - Reusing from Settings page for consistency |
| 16 |
const OpenAIIcon = () => ( |
| 17 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> |
| 18 |
<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"/> |
| 19 |
</svg> |
| 20 |
); |
| 21 |
|
| 22 |
const ClaudeIcon = () => ( |
| 23 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> |
| 24 |
<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"/> |
| 25 |
</svg> |
| 26 |
); |
| 27 |
|
| 28 |
/** |
| 29 |
* Dashboard Component |
| 30 |
*/ |
| 31 |
const Dashboard = () => { |
| 32 |
return ( |
| 33 |
<div className="thinkrank-dashboard"> |
| 34 |
<OverviewTab /> |
| 35 |
</div> |
| 36 |
); |
| 37 |
}; |
| 38 |
|
| 39 |
/** |
| 40 |
* Overview Tab Component |
| 41 |
*/ |
| 42 |
const OverviewTab = () => { |
| 43 |
const [settings, setSettings] = useState(null); |
| 44 |
const [briefCount, setBriefCount] = useState(0); |
| 45 |
const [isLoading, setIsLoading] = useState(true); |
| 46 |
|
| 47 |
// Helper function to check if API is actually configured |
| 48 |
const isApiConfigured = (settings) => { |
| 49 |
if (!settings?.ai_provider) return false; |
| 50 |
|
| 51 |
const apiKeyField = settings.ai_provider === 'openai' ? 'openai_api_key' : 'claude_api_key'; |
| 52 |
const apiKey = settings[apiKeyField]; |
| 53 |
|
| 54 |
// Check if API key exists - masked keys (••••••••) indicate a saved key |
| 55 |
return apiKey && apiKey.trim() && (apiKey.includes('••••') || apiKey.length > 10); |
| 56 |
}; |
| 57 |
|
| 58 |
useEffect(() => { |
| 59 |
const loadDashboardData = async () => { |
| 60 |
try { |
| 61 |
// Load settings to show API connection status |
| 62 |
const settingsResponse = await apiFetch({ |
| 63 |
path: '/thinkrank/v1/settings', |
| 64 |
method: 'GET', |
| 65 |
}); |
| 66 |
|
| 67 |
setSettings(settingsResponse); |
| 68 |
|
| 69 |
// Load brief count |
| 70 |
const briefsResponse = await apiFetch({ |
| 71 |
path: '/thinkrank/v1/content-brief/list', |
| 72 |
method: 'GET', |
| 73 |
}); |
| 74 |
setBriefCount(briefsResponse?.data?.length || 0); |
| 75 |
|
| 76 |
} catch (error) { |
| 77 |
console.error('Failed to load dashboard data:', error); |
| 78 |
} finally { |
| 79 |
setIsLoading(false); |
| 80 |
} |
| 81 |
}; |
| 82 |
|
| 83 |
loadDashboardData(); |
| 84 |
|
| 85 |
// Also refresh when the component becomes visible (e.g., tab switch) |
| 86 |
const handleVisibilityChange = () => { |
| 87 |
if (!document.hidden) { |
| 88 |
loadDashboardData(); |
| 89 |
} |
| 90 |
}; |
| 91 |
|
| 92 |
document.addEventListener('visibilitychange', handleVisibilityChange); |
| 93 |
|
| 94 |
return () => { |
| 95 |
document.removeEventListener('visibilitychange', handleVisibilityChange); |
| 96 |
}; |
| 97 |
}, []); |
| 98 |
|
| 99 |
if (isLoading) { |
| 100 |
return ( |
| 101 |
<div className="thinkrank-flex-center thinkrank-p-8"> |
| 102 |
<Spinner /> |
| 103 |
</div> |
| 104 |
); |
| 105 |
} |
| 106 |
|
| 107 |
// Professional icons for dashboard cards |
| 108 |
const CheckIcon = () => ( |
| 109 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> |
| 110 |
<path d="M20 6L9 17l-5-5"/> |
| 111 |
</svg> |
| 112 |
); |
| 113 |
|
| 114 |
const DocumentIcon = () => ( |
| 115 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> |
| 116 |
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> |
| 117 |
<polyline points="14,2 14,8 20,8"/> |
| 118 |
</svg> |
| 119 |
); |
| 120 |
|
| 121 |
const StatusIcon = () => ( |
| 122 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> |
| 123 |
<circle cx="12" cy="12" r="10"/> |
| 124 |
<path d="M9 12l2 2 4-4"/> |
| 125 |
</svg> |
| 126 |
); |
| 127 |
|
| 128 |
return ( |
| 129 |
<div className="thinkrank-ui"> |
| 130 |
<div className="thinkrank-card thinkrank-card--elevated thinkrank-mb-6"> |
| 131 |
<div className="thinkrank-card__header"> |
| 132 |
<h2 className="thinkrank-text-2xl thinkrank-font-bold thinkrank-text-primary thinkrank-mb-2"> |
| 133 |
{__('Dashboard Overview', 'thinkrank')} |
| 134 |
</h2> |
| 135 |
<p className="thinkrank-text-secondary thinkrank-text-base"> |
| 136 |
{__('Your AI-powered content brief generator status and statistics.', 'thinkrank')} |
| 137 |
</p> |
| 138 |
</div> |
| 139 |
<div className="thinkrank-card__body"> |
| 140 |
<div className="thinkrank-grid thinkrank-grid-cols-1-xs thinkrank-grid-cols-2-md thinkrank-grid-cols-3-lg thinkrank-gap-6"> |
| 141 |
{/* AI Status Card */} |
| 142 |
<div className={`thinkrank-card thinkrank-card--${settings?.ai_provider === 'claude' ? 'warning' : settings?.ai_provider === 'openai' ? 'success' : 'flat'} thinkrank-relative`}> |
| 143 |
{/* Status Badge */} |
| 144 |
<div className="thinkrank-absolute thinkrank-top-3 thinkrank-right-3 thinkrank-z-10"> |
| 145 |
<span className={`thinkrank-px-2 thinkrank-py-1 thinkrank-text-xs thinkrank-font-medium thinkrank-rounded-full ${ |
| 146 |
isApiConfigured(settings) |
| 147 |
? 'thinkrank-bg-green thinkrank-text-white' |
| 148 |
: 'thinkrank-bg-gray-200 thinkrank-text-gray-700' |
| 149 |
}`}> |
| 150 |
{isApiConfigured(settings) ? __('Connected', 'thinkrank') : __('Not Connected', 'thinkrank')} |
| 151 |
</span> |
| 152 |
</div> |
| 153 |
|
| 154 |
<div className="thinkrank-card__body"> |
| 155 |
<div className="thinkrank-flex thinkrank-items-center thinkrank-gap-4"> |
| 156 |
<div className={`thinkrank-w-12 thinkrank-h-12 thinkrank-rounded-lg thinkrank-flex-center thinkrank-p-3 ${ |
| 157 |
settings?.ai_provider === 'claude' |
| 158 |
? 'thinkrank-bg-orange thinkrank-text-white' |
| 159 |
: settings?.ai_provider === 'openai' |
| 160 |
? 'thinkrank-bg-green thinkrank-text-white' |
| 161 |
: 'thinkrank-bg-gray-100 thinkrank-text-gray-500' |
| 162 |
}`}> |
| 163 |
{isApiConfigured(settings) && settings?.ai_provider === 'claude' ? ( |
| 164 |
<ClaudeIcon /> |
| 165 |
) : isApiConfigured(settings) && settings?.ai_provider === 'openai' ? ( |
| 166 |
<OpenAIIcon /> |
| 167 |
) : ( |
| 168 |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> |
| 169 |
<circle cx="12" cy="12" r="10"/> |
| 170 |
<path d="M15 9l-6 6M9 9l6 6"/> |
| 171 |
</svg> |
| 172 |
)} |
| 173 |
</div> |
| 174 |
<div className="thinkrank-flex-1"> |
| 175 |
<h3 className="thinkrank-text-xs thinkrank-font-semibold thinkrank-text-muted thinkrank-uppercase thinkrank-tracking-wide thinkrank-mb-1"> |
| 176 |
{__('AI Provider', 'thinkrank')} |
| 177 |
</h3> |
| 178 |
{isApiConfigured(settings) ? ( |
| 179 |
<> |
| 180 |
<div className="thinkrank-text-lg thinkrank-font-bold thinkrank-text-primary thinkrank-mb-1"> |
| 181 |
{settings.ai_provider === 'openai' ? 'OpenAI' : (settings.ai_provider === 'claude' ? 'Claude' : 'Gemini')} |
| 182 |
</div> |
| 183 |
<div className="thinkrank-text-sm thinkrank-text-secondary"> |
| 184 |
{(() => { |
| 185 |
const model = settings.ai_provider === 'claude' ? settings.claude_model : (settings.ai_provider === 'gemini' ? settings.gemini_model : settings.openai_model); |
| 186 |
|
| 187 |
// Use shared ModelBadge display names |
| 188 |
switch (model) { |
| 189 |
case 'gpt-5-nano': return 'GPT‑5‑nano'; |
| 190 |
case 'gpt-5-mini': return 'GPT‑5‑mini'; |
| 191 |
case 'gpt-5': return 'GPT‑5'; |
| 192 |
case 'gpt-4o': return 'GPT‑4o'; |
| 193 |
case 'claude-sonnet-4-0': |
| 194 |
case 'claude-sonnet-4-20250514': return 'Claude 4 Sonnet'; |
| 195 |
case 'claude-opus-4-0': |
| 196 |
case 'claude-opus-4-20250514': return 'Claude 4 Opus'; |
| 197 |
case 'claude-3-7-sonnet-latest': |
| 198 |
case 'claude-3-7-sonnet-20250219': return 'Claude 3.7 Sonnet'; |
| 199 |
case 'claude-3-5-sonnet-latest': |
| 200 |
case 'claude-3-5-sonnet-20241022': return 'Claude 3.5 Sonnet'; |
| 201 |
case 'claude-3-5-haiku-latest': |
| 202 |
case 'claude-3-5-haiku-20241022': return 'Claude 3.5 Haiku'; |
| 203 |
case 'gemini-2.5-flash': return 'Gemini 2.5 Flash'; |
| 204 |
case 'gemini-2.5-flash-lite': return 'Gemini 2.5 Flash-Lite'; |
| 205 |
case 'gemini-2.5-pro': return 'Gemini 2.5 Pro'; |
| 206 |
case 'gemini-2.0-flash': return 'Gemini 2.0 Flash'; |
| 207 |
case 'gemini-1.5-flash': return 'Gemini 1.5 Flash'; |
| 208 |
default: return model; |
| 209 |
} |
| 210 |
return model || 'Not configured'; |
| 211 |
})()} |
| 212 |
</div> |
| 213 |
</> |
| 214 |
) : ( |
| 215 |
<> |
| 216 |
<div className="thinkrank-text-lg thinkrank-font-bold thinkrank-text-primary thinkrank-mb-1"> |
| 217 |
{__('Not Configured', 'thinkrank')} |
| 218 |
</div> |
| 219 |
<div className="thinkrank-text-sm thinkrank-text-secondary"> |
| 220 |
{__('Configure API settings', 'thinkrank')} |
| 221 |
</div> |
| 222 |
</> |
| 223 |
)} |
| 224 |
</div> |
| 225 |
</div> |
| 226 |
</div> |
| 227 |
</div> |
| 228 |
|
| 229 |
{/* Content Briefs Card */} |
| 230 |
<div className="thinkrank-card thinkrank-relative"> |
| 231 |
{/* Status Badge */} |
| 232 |
<div className="thinkrank-absolute thinkrank-top-4 thinkrank-right-4"> |
| 233 |
<span className="thinkrank-px-2 thinkrank-py-1 thinkrank-text-xs thinkrank-font-medium thinkrank-rounded-full thinkrank-bg-blue thinkrank-text-white"> |
| 234 |
{briefCount > 0 ? __('Active', 'thinkrank') : __('Empty', 'thinkrank')} |
| 235 |
</span> |
| 236 |
</div> |
| 237 |
|
| 238 |
<div className="thinkrank-card__body"> |
| 239 |
<div className="thinkrank-flex thinkrank-items-center thinkrank-gap-4"> |
| 240 |
<div className="thinkrank-w-12 thinkrank-h-12 thinkrank-rounded-lg thinkrank-bg-blue thinkrank-text-white thinkrank-flex-center thinkrank-p-3"> |
| 241 |
<DocumentIcon /> |
| 242 |
</div> |
| 243 |
<div className="thinkrank-flex-1"> |
| 244 |
<h3 className="thinkrank-text-xs thinkrank-font-semibold thinkrank-text-muted thinkrank-uppercase thinkrank-tracking-wide thinkrank-mb-1"> |
| 245 |
{__('Content Briefs', 'thinkrank')} |
| 246 |
</h3> |
| 247 |
<div className="thinkrank-text-3xl thinkrank-font-bold thinkrank-text-primary thinkrank-mb-1"> |
| 248 |
{briefCount} |
| 249 |
</div> |
| 250 |
<div className="thinkrank-text-sm thinkrank-text-secondary"> |
| 251 |
{__('Total Created', 'thinkrank')} |
| 252 |
</div> |
| 253 |
</div> |
| 254 |
</div> |
| 255 |
</div> |
| 256 |
</div> |
| 257 |
|
| 258 |
{/* System Status Card */} |
| 259 |
<div className={`thinkrank-card thinkrank-card--${isApiConfigured(settings) ? 'success' : 'warning'} thinkrank-relative`}> |
| 260 |
{/* Status Badge */} |
| 261 |
<div className="thinkrank-absolute thinkrank-top-4 thinkrank-right-4"> |
| 262 |
<span className={`thinkrank-px-2 thinkrank-py-1 thinkrank-text-xs thinkrank-font-medium thinkrank-rounded-full ${ |
| 263 |
isApiConfigured(settings) |
| 264 |
? 'thinkrank-bg-green thinkrank-text-white' |
| 265 |
: 'thinkrank-bg-orange thinkrank-text-white' |
| 266 |
}`}> |
| 267 |
{isApiConfigured(settings) ? __('Ready', 'thinkrank') : __('Setup Required', 'thinkrank')} |
| 268 |
</span> |
| 269 |
</div> |
| 270 |
|
| 271 |
<div className="thinkrank-card__body"> |
| 272 |
<div className="thinkrank-flex thinkrank-items-center thinkrank-gap-4"> |
| 273 |
<div className={`thinkrank-w-12 thinkrank-h-12 thinkrank-rounded-lg thinkrank-flex-center thinkrank-p-3 ${ |
| 274 |
isApiConfigured(settings) |
| 275 |
? 'thinkrank-bg-green thinkrank-text-white' |
| 276 |
: 'thinkrank-bg-orange thinkrank-text-white' |
| 277 |
}`}> |
| 278 |
<StatusIcon /> |
| 279 |
</div> |
| 280 |
<div className="thinkrank-flex-1"> |
| 281 |
<h3 className="thinkrank-text-xs thinkrank-font-semibold thinkrank-text-muted thinkrank-uppercase thinkrank-tracking-wide thinkrank-mb-1"> |
| 282 |
{__('System Status', 'thinkrank')} |
| 283 |
</h3> |
| 284 |
<div className="thinkrank-text-lg thinkrank-font-bold thinkrank-text-primary thinkrank-mb-1"> |
| 285 |
{isApiConfigured(settings) ? __('Operational', 'thinkrank') : __('Setup Required', 'thinkrank')} |
| 286 |
</div> |
| 287 |
<div className="thinkrank-text-sm thinkrank-text-secondary"> |
| 288 |
{isApiConfigured(settings) ? __('All systems running smoothly', 'thinkrank') : __('Configure API to get started', 'thinkrank')} |
| 289 |
</div> |
| 290 |
</div> |
| 291 |
</div> |
| 292 |
</div> |
| 293 |
</div> |
| 294 |
</div> |
| 295 |
</div> |
| 296 |
</div> |
| 297 |
</div> |
| 298 |
); |
| 299 |
}; |
| 300 |
|
| 301 |
export default Dashboard; |
| 302 |
|