ALwrity LinkedIn Writer: Billing Dashboard: Compact View, Billing Overview, System Health Indicator, Cost Breakdown, Usage Trends, Usage Alerts, Comprehensive API Breakdown

This commit is contained in:
ajaysi
2025-09-11 11:09:10 +05:30
parent b156298e82
commit 1b65a9487b
84 changed files with 10143 additions and 156 deletions

View File

@@ -203,7 +203,7 @@ const MonitoringCharts: React.FC<MonitoringChartsProps> = ({
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${((percent || 0) * 100).toFixed(0)}%`}
label={({ name, value }) => `${name} ${value}`}
outerRadius={80}
fill="#8884d8"
dataKey="value"

View File

@@ -0,0 +1,337 @@
import React, { useState } from 'react';
interface FeatureCard {
title: string;
desc: string;
icon: string;
image?: string;
onClick?: () => void;
}
interface FeatureCarouselProps {
onFactCheckClick: () => void;
onCopilotClick: () => void;
}
export const FeatureCarousel: React.FC<FeatureCarouselProps> = ({
onFactCheckClick,
onCopilotClick
}) => {
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const featureCards: FeatureCard[] = [
{
title: 'Check Facts',
desc: 'Select text and verify claims with web-backed evidence.',
icon: '🔍',
image: '/Alwrity-fact-check.png',
onClick: onFactCheckClick
},
{
title: 'Google-Grounded Search',
desc: 'Use native Google grounding to inform content with current sources.',
icon: '🌐'
},
{
title: 'Persona-Aware Writing',
desc: 'Generate content tailored to your writing persona and audience.',
icon: '👤'
},
{
title: 'Assistive Writing',
desc: 'Inline, contextual suggestions as you type with citations.',
icon: '✍️',
image: '/ALwrity-assistive-writing.png'
},
{
title: 'ALwrity Copilot',
desc: 'Advanced AI assistant for comprehensive content creation and editing.',
icon: '🤖',
image: '/Alwrity-copilot1.png',
onClick: onCopilotClick
},
{
title: 'Multimodal Generation',
desc: 'Create content with images, videos, and interactive elements.',
icon: '🎨'
}
];
const nextCard = () => {
setCurrentCardIndex((prev) => {
const maxIndex = Math.max(0, featureCards.length - 3);
return prev >= maxIndex ? 0 : prev + 3;
});
};
const prevCard = () => {
setCurrentCardIndex((prev) => {
const maxIndex = Math.max(0, featureCards.length - 3);
return prev <= 0 ? maxIndex : prev - 3;
});
};
return (
<div style={{
marginBottom: 20,
width: '100%',
maxWidth: 1200,
position: 'relative',
padding: '10px 0'
}}>
{/* Carousel Container with Enhanced Styling */}
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '20px',
padding: '12px',
boxShadow: `
0 20px 60px rgba(0,0,0,0.15),
0 8px 32px rgba(102, 126, 234, 0.1),
inset 0 1px 0 rgba(255,255,255,0.2)
`,
position: 'relative',
overflow: 'hidden'
}}>
{/* Background Glow Effect */}
<div style={{
position: 'absolute',
top: '-50%',
left: '-50%',
width: '200%',
height: '200%',
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
animation: 'rotate 20s linear infinite',
zIndex: 0
}} />
{/* Compact Navigation - Positioned on the sides */}
<button
onClick={prevCard}
style={{
position: 'absolute',
left: '8px',
top: '50%',
transform: 'translateY(-50%)',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '50%',
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)',
transition: 'all 0.3s ease',
color: 'white',
fontSize: '14px',
fontWeight: 'bold',
zIndex: 3
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6), 0 3px 12px rgba(0,0,0,0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1)';
e.currentTarget.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)';
}}
>
</button>
<button
onClick={nextCard}
style={{
position: 'absolute',
right: '8px',
top: '50%',
transform: 'translateY(-50%)',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '50%',
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)',
transition: 'all 0.3s ease',
color: 'white',
fontSize: '14px',
fontWeight: 'bold',
zIndex: 3
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1.1)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6), 0 3px 12px rgba(0,0,0,0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(-50%) scale(1)';
e.currentTarget.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4), 0 2px 8px rgba(0,0,0,0.1)';
}}
>
</button>
{/* Features Grid - 3 at a time */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '12px',
zIndex: 2,
position: 'relative'
}}>
{featureCards.slice(currentCardIndex, currentCardIndex + 3).map((card, index) => (
<div
key={currentCardIndex + index}
onClick={card.onClick}
title={card.desc}
style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(15px)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '16px',
padding: '16px',
boxShadow: `
0 12px 40px rgba(0,0,0,0.1),
0 4px 20px rgba(102, 126, 234, 0.1),
inset 0 1px 0 rgba(255,255,255,0.3)
`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
position: 'relative',
overflow: 'hidden',
transition: 'all 0.3s ease',
minHeight: '140px',
cursor: card.onClick ? 'pointer' : 'default'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-8px) scale(1.02)';
e.currentTarget.style.boxShadow = `
0 20px 60px rgba(0,0,0,0.15),
0 8px 30px rgba(102, 126, 234, 0.2),
inset 0 1px 0 rgba(255,255,255,0.4)
`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.boxShadow = `
0 12px 40px rgba(0,0,0,0.1),
0 4px 20px rgba(102, 126, 234, 0.1),
inset 0 1px 0 rgba(255,255,255,0.3)
`;
}}
>
{/* Card Background Pattern */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `linear-gradient(45deg,
rgba(102, 126, 234, ${0.1 + index * 0.05}) 0%,
rgba(118, 75, 162, ${0.1 + index * 0.05}) 100%)`,
opacity: 0.4
}} />
{/* Icon/Image - Much Larger */}
<div style={{
fontSize: '48px',
marginBottom: '8px',
zIndex: 1,
filter: 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100px',
flex: '1'
}}>
{card.image ? (
<img
src={card.image}
alt={card.title}
style={{
width: '95%',
height: '100%',
objectFit: 'contain',
borderRadius: '8px'
}}
/>
) : (
<div style={{ fontSize: '64px' }}>
{card.icon}
</div>
)}
</div>
{/* Title Only - Description moved to tooltip */}
<h4 style={{
margin: '0',
color: '#1a202c',
fontSize: '14px',
fontWeight: '700',
zIndex: 1,
textShadow: '0 2px 4px rgba(0,0,0,0.1)',
textAlign: 'center',
lineHeight: '1.2',
padding: '0 4px'
}}>
{card.title}
</h4>
</div>
))}
</div>
{/* Enhanced Dots Indicator */}
<div style={{
display: 'flex',
justifyContent: 'center',
gap: '10px',
marginTop: '12px',
zIndex: 2,
position: 'relative'
}}>
{Array.from({ length: Math.ceil(featureCards.length / 3) }).map((_, index) => (
<button
key={index}
onClick={() => setCurrentCardIndex(index * 3)}
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
border: 'none',
background: Math.floor(currentCardIndex / 3) === index
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
: 'rgba(255,255,255,0.3)',
cursor: 'pointer',
transition: 'all 0.3s ease',
boxShadow: Math.floor(currentCardIndex / 3) === index
? '0 3px 12px rgba(102, 126, 234, 0.4)'
: '0 2px 6px rgba(0,0,0,0.1)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.2)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.5)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)';
e.currentTarget.style.boxShadow = Math.floor(currentCardIndex / 3) === index
? '0 3px 12px rgba(102, 126, 234, 0.4)'
: '0 2px 6px rgba(0,0,0,0.1)';
}}
/>
))}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,493 @@
import React from 'react';
interface InfoModalsProps {
showCopilotModal: boolean;
showAssistiveModal: boolean;
showFactCheckModal: boolean;
onCloseCopilotModal: () => void;
onCloseAssistiveModal: () => void;
onCloseFactCheckModal: () => void;
onOpenCopilot: () => void;
}
export const InfoModals: React.FC<InfoModalsProps> = ({
showCopilotModal,
showAssistiveModal,
showFactCheckModal,
onCloseCopilotModal,
onCloseAssistiveModal,
onCloseFactCheckModal,
onOpenCopilot
}) => {
return (
<>
{/* Copilot Modal */}
{showCopilotModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(5px)'
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.9) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '24px',
padding: '32px',
maxWidth: '800px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative'
}}>
<button
onClick={onCloseCopilotModal}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'rgba(0,0,0,0.1)',
border: 'none',
borderRadius: '50%',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
color: '#666'
}}
>
×
</button>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<h2 style={{ margin: '0 0 16px 0', color: '#1a202c', fontSize: '24px', fontWeight: '700' }}>
ALwrity Copilot
</h2>
<p style={{ margin: '0 0 20px 0', color: '#4a5568', fontSize: '16px' }}>
Your comprehensive AI writing assistant
</p>
{/* Screenshot Images */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '16px',
marginBottom: '20px'
}}>
<div style={{ textAlign: 'center' }}>
<img
src="/Alwrity-copilot1.png"
alt="ALwrity Copilot Interface"
style={{
width: '100%',
maxWidth: '250px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0'
}}
/>
<p style={{
margin: '8px 0 0 0',
fontSize: '12px',
color: '#666',
fontWeight: '500'
}}>
Main Interface
</p>
</div>
<div style={{ textAlign: 'center' }}>
<img
src="/Alwrity-copilot2.png"
alt="ALwrity Copilot Features"
style={{
width: '100%',
maxWidth: '250px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0'
}}
/>
<p style={{
margin: '8px 0 0 0',
fontSize: '12px',
color: '#666',
fontWeight: '500'
}}>
Advanced Features
</p>
</div>
</div>
</div>
<div style={{ textAlign: 'left' }}>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
What is ALwrity Copilot?
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
ALwrity Copilot is an advanced AI assistant that provides comprehensive support for all your content creation needs.
It combines multiple AI capabilities to help you create, edit, and optimize content across various formats.
</p>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
Key Features:
</h3>
<ul style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li>Generate LinkedIn posts, articles, carousels, and video scripts</li>
<li>Real-time content editing and optimization suggestions</li>
<li>Research-backed content with source citations</li>
<li>Persona-aware writing tailored to your audience</li>
<li>Fact-checking and verification capabilities</li>
<li>Multi-format content creation (text, images, videos)</li>
</ul>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
How to Use:
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
Click the ALwrity Copilot icon in the bottom-right corner of your screen to open the chat interface.
You can then ask for help with any content creation task, and the AI will guide you through the process.
</p>
<button
onClick={() => {
onCloseCopilotModal();
onOpenCopilot();
}}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(102, 126, 234, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Open ALwrity Copilot
</button>
</div>
</div>
</div>
)}
{/* Assistive Research Modal */}
{showAssistiveModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(5px)'
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.9) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '24px',
padding: '32px',
maxWidth: '600px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative'
}}>
<button
onClick={onCloseAssistiveModal}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'rgba(0,0,0,0.1)',
border: 'none',
borderRadius: '50%',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
color: '#666'
}}
>
×
</button>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔬</div>
<h2 style={{ margin: '0 0 8px 0', color: '#1a202c', fontSize: '24px', fontWeight: '700' }}>
Assistive Research Writing
</h2>
<p style={{ margin: 0, color: '#4a5568', fontSize: '16px' }}>
Real-time AI writing assistance with research-backed suggestions
</p>
</div>
<div style={{ textAlign: 'left' }}>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
What is Assistive Research Writing?
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
Assistive Research Writing provides real-time, contextual writing suggestions as you type.
It combines AI-powered content generation with web research to provide accurate, up-to-date information
and suggestions that enhance your writing quality and credibility.
</p>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
Key Features:
</h3>
<ul style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li>Real-time writing suggestions as you type</li>
<li>Research-backed content with source citations</li>
<li>Contextual continuation of your thoughts</li>
<li>Fact-checking and verification of claims</li>
<li>Smart gating to prevent excessive API usage</li>
<li>Seamless integration with your writing flow</li>
</ul>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
How to Use:
</h3>
<p style={{ color: '#4a5568', lineHeight: '1.6', marginBottom: '20px' }}>
Enable Assistive Writing in the editor settings. Once enabled, start typing your content.
After typing 5+ words and pausing for 5 seconds, you'll receive contextual writing suggestions.
You can accept, dismiss, or request more suggestions as needed.
</p>
<button
onClick={onCloseAssistiveModal}
style={{
background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(240, 147, 251, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Got it, let's start writing!
</button>
</div>
</div>
</div>
)}
{/* Fact Check Modal */}
{showFactCheckModal && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
backdropFilter: 'blur(5px)'
}}>
<div style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.9) 100%)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255,255,255,0.3)',
borderRadius: '24px',
padding: '32px',
maxWidth: '800px',
width: '90%',
maxHeight: '80vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
position: 'relative'
}}>
<button
onClick={onCloseFactCheckModal}
style={{
position: 'absolute',
top: '16px',
right: '16px',
background: 'rgba(0,0,0,0.1)',
border: 'none',
borderRadius: '50%',
width: '32px',
height: '32px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '18px',
color: '#666'
}}
>
×
</button>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🔍</div>
<h2 style={{ margin: '0 0 8px 0', color: '#1a202c', fontSize: '24px', fontWeight: '700' }}>
Check Facts Feature
</h2>
<p style={{ margin: 0, color: '#4a5568', fontSize: '16px' }}>
Verify claims with web-backed evidence and AI-powered analysis
</p>
</div>
{/* Images Section */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
gap: '20px',
marginBottom: '24px'
}}>
<div style={{
background: 'rgba(255,255,255,0.5)',
borderRadius: '12px',
padding: '16px',
textAlign: 'center'
}}>
<img
src="/Alwrity-fact-check.png"
alt="ALwrity Fact Check Interface"
style={{
width: '100%',
maxWidth: '300px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 15px rgba(0,0,0,0.1)',
marginBottom: '12px'
}}
/>
<h4 style={{ margin: '0 0 8px 0', color: '#2d3748', fontSize: '16px', fontWeight: '600' }}>
ALwrity Fact Check Interface
</h4>
<p style={{ margin: 0, color: '#4a5568', fontSize: '14px' }}>
Select any text in your content to verify claims
</p>
</div>
<div style={{
background: 'rgba(255,255,255,0.5)',
borderRadius: '12px',
padding: '16px',
textAlign: 'center'
}}>
<img
src="/Fact-check1.png"
alt="Fact Check Results"
style={{
width: '100%',
maxWidth: '300px',
height: 'auto',
borderRadius: '8px',
boxShadow: '0 4px 15px rgba(0,0,0,0.1)',
marginBottom: '12px'
}}
/>
<h4 style={{ margin: '0 0 8px 0', color: '#2d3748', fontSize: '16px', fontWeight: '600' }}>
Detailed Fact Check Results
</h4>
<p style={{ margin: 0, color: '#4a5568', fontSize: '14px' }}>
Get comprehensive analysis with source citations
</p>
</div>
</div>
<div style={{ textAlign: 'left' }}>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
How Fact Checking Works:
</h3>
<ol style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li><strong>Select Text:</strong> Highlight any claim or statement in your content</li>
<li><strong>AI Analysis:</strong> Our AI extracts key claims and identifies fact-checkable statements</li>
<li><strong>Web Search:</strong> Search for evidence using Exa.ai and Google Search</li>
<li><strong>Verification:</strong> Compare claims against reliable sources and evidence</li>
<li><strong>Results:</strong> Get detailed analysis with confidence scores and source citations</li>
</ol>
<h3 style={{ color: '#2d3748', fontSize: '18px', fontWeight: '600', marginBottom: '12px' }}>
Key Benefits:
</h3>
<ul style={{ color: '#4a5568', lineHeight: '1.6', paddingLeft: '20px', marginBottom: '20px' }}>
<li>Verify claims before publishing to maintain credibility</li>
<li>Get source citations for better content transparency</li>
<li>Identify potentially misleading or false information</li>
<li>Enhance content quality with evidence-based writing</li>
<li>Build trust with your audience through verified content</li>
</ul>
<button
onClick={onCloseFactCheckModal}
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none',
borderRadius: '12px',
padding: '12px 24px',
color: 'white',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
width: '100%',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(102, 126, 234, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Got it, let's start fact-checking!
</button>
</div>
</div>
</div>
)}
</>
);
};

View File

@@ -32,6 +32,7 @@ import AnalyzePillarChips from './components/AnalyzePillarChips';
import EngagePillarChips from './components/EngagePillarChips';
import EnhancedTodayChip from './components/EnhancedTodayChip';
import OnboardingModal from './components/OnboardingModal';
import WorkflowHeroSection from './components/WorkflowHeroSection';
import { pillarData } from './components/PillarData';
import { useWorkflowStore } from '../../stores/workflowStore';
@@ -487,6 +488,14 @@ const ContentLifecyclePillars: React.FC = () => {
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [onboardingModalOpen, setOnboardingModalOpen] = useState(false);
// Workflow store hooks
const {
currentWorkflow,
workflowProgress,
isLoading: workflowLoading,
startWorkflow,
} = useWorkflowStore();
const handleOnboardingClick = () => {
setOnboardingModalOpen(true);
};
@@ -495,6 +504,20 @@ const ContentLifecyclePillars: React.FC = () => {
setOnboardingModalOpen(false);
};
const handleStartWorkflow = async () => {
try {
if (currentWorkflow) {
await startWorkflow(currentWorkflow.id);
}
} catch (error) {
console.error('Failed to start workflow:', error);
}
};
// Check if workflow is active (in progress or completed)
const isWorkflowActive = currentWorkflow?.workflowStatus === 'in_progress' ||
currentWorkflow?.workflowStatus === 'completed';
return (
<>
<Box
@@ -503,7 +526,8 @@ const ContentLifecyclePillars: React.FC = () => {
background: 'linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
backdropFilter: 'blur(8px)',
borderRadius: 2,
mb: 4
mb: 4,
position: 'relative', // For hero section positioning
}}
>
<Container maxWidth="xl">
@@ -530,6 +554,13 @@ const ContentLifecyclePillars: React.FC = () => {
))}
</Box>
</Container>
{/* Hero Section Overlay */}
<WorkflowHeroSection
onStartWorkflow={handleStartWorkflow}
isWorkflowActive={isWorkflowActive}
isLoading={workflowLoading}
/>
</Box>
{/* Onboarding Modal */}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import {
Box,
Container,
@@ -24,6 +24,8 @@ import EmptyState from '../shared/EmptyState';
import ContentLifecyclePillars from './ContentLifecyclePillars';
import AnalyticsInsights from './components/AnalyticsInsights';
import ToolsModal from './components/ToolsModal';
import EnhancedBillingDashboard from '../billing/EnhancedBillingDashboard';
import CompactSidebar from './components/CompactSidebar';
// Shared types and utilities
import { Tool } from '../shared/types';
@@ -41,6 +43,9 @@ const MainDashboard: React.FC = () => {
const theme = useTheme();
const navigate = useNavigate();
// Sidebar state
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
// Zustand store hooks
const {
loading,
@@ -272,7 +277,13 @@ const MainDashboard: React.FC = () => {
},
}}
>
<Container maxWidth="xl" sx={{ position: 'relative', zIndex: 1 }}>
<Container
maxWidth="xl"
sx={{
position: 'relative',
zIndex: 1,
}}
>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 20 }}
@@ -302,22 +313,39 @@ const MainDashboard: React.FC = () => {
{/* Content Lifecycle Pillars - First Panel */}
<ContentLifecyclePillars />
{/* Search and Filter */}
<SearchFilter
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
onClearSearch={() => setSearchQuery('')}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
selectedSubCategory={selectedSubCategory}
onSubCategoryChange={setSelectedSubCategory}
toolCategories={toolCategories}
theme={theme}
onCategoryClick={handleCategoryClick}
/>
{/* Side-by-side layout for Areas 2 and 3 */}
<Box sx={{ display: 'flex', gap: 3, mt: 3 }}>
{/* Area 2: Search Tools Sidebar */}
<Box sx={{
width: sidebarCollapsed ? 60 : 280,
transition: 'width 0.3s ease-in-out',
flexShrink: 0
}}>
<CompactSidebar
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
onClearSearch={() => setSearchQuery('')}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
selectedSubCategory={selectedSubCategory}
onSubCategoryChange={setSelectedSubCategory}
toolCategories={toolCategories}
onCategoryClick={handleCategoryClick}
collapsed={sidebarCollapsed}
onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
theme={theme}
/>
</Box>
{/* Analytics Insights - Good/Bad/Ugly */}
<AnalyticsInsights />
{/* Area 3: Analytics and Billing */}
<Box sx={{ flex: 1, minWidth: 0 }}>
{/* Analytics Insights - Good/Bad/Ugly */}
<AnalyticsInsights />
{/* Billing & Usage Dashboard */}
<EnhancedBillingDashboard />
</Box>
</Box>
{/* Tools Modal */}
<ToolsModal

View File

@@ -263,7 +263,7 @@ const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionCli
};
return (
<Box sx={{ mt: 2, mb: 2.9 }}>
<Box sx={{ mt: 1, mb: 1.5 }}>
<Box sx={{ position: 'relative', overflow: 'hidden' }}>
<Typography
variant="h6"
@@ -277,7 +277,7 @@ const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionCli
Today's Analytics Insights
</Typography>
</Box>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1.5}>
<Stack direction={{ xs: 'column', md: 'row' }} spacing={1}>
{columns.map((col) => {
const isHovered = hovered === col.key;
const visibleItems = isHovered ? col.items : col.items.slice(0, 1);
@@ -291,16 +291,16 @@ const AnalyticsInsights: React.FC<AnalyticsInsightsProps> = ({ data, onActionCli
<Badge>{col.items.length}</Badge>
</GradientHeader>
<CardContent sx={{ p: 1.5 }}>
<Stack spacing={1}>
<CardContent sx={{ p: 1, '&:last-child': { pb: 1 } }}>
<Stack spacing={0.5}>
{visibleItems.map((insight) => (
<Box key={insight.id} sx={{
background: 'rgba(255,255,255,0.08)',
border: '1px solid rgba(255,255,255,0.18)',
borderRadius: 1.5,
p: 1
p: 0.8
}}>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mb: 0.25 }}>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mb: 0.1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.95)', fontWeight: 700, fontSize: '0.8rem' }}>
{insight.title}
</Typography>

View File

@@ -0,0 +1,685 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box,
Paper,
Typography,
Chip,
IconButton,
Tooltip,
Divider,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
Search,
Filter,
Settings,
ChevronLeft,
ChevronRight,
Activity,
Zap
} from 'lucide-react';
// Shared components
import SearchFilter from '../../shared/SearchFilter';
// Types
import { ToolCategories } from '../../shared/types';
interface CompactSidebarProps {
searchQuery: string;
onSearchChange: (query: string) => void;
onClearSearch: () => void;
selectedCategory: string | null;
onCategoryChange: (category: string | null) => void;
selectedSubCategory: string | null;
onSubCategoryChange: (subCategory: string | null) => void;
toolCategories: ToolCategories;
onCategoryClick: (categoryName: string | null, categoryData?: any) => void;
collapsed: boolean;
onToggleCollapse: () => void;
theme: any;
}
// Session control for animation
const SIDEBAR_ANIMATION_KEY = 'sidebar_animation_shown';
const ANIMATION_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const shouldShowAnimation = (): boolean => {
const lastShown = localStorage.getItem(SIDEBAR_ANIMATION_KEY);
if (!lastShown) return true;
const lastShownTime = parseInt(lastShown, 10);
const now = Date.now();
return (now - lastShownTime) > ANIMATION_COOLDOWN;
};
const markAnimationShown = (): void => {
localStorage.setItem(SIDEBAR_ANIMATION_KEY, Date.now().toString());
};
const CompactSidebar: React.FC<CompactSidebarProps> = ({
searchQuery,
onSearchChange,
onClearSearch,
selectedCategory,
onCategoryChange,
selectedSubCategory,
onSubCategoryChange,
toolCategories,
onCategoryClick,
collapsed,
onToggleCollapse,
theme
}) => {
const [isAnimating, setIsAnimating] = useState(false);
const [rippleIndex, setRippleIndex] = useState(-1);
const [shouldAutoExpand, setShouldAutoExpand] = useState(false);
const [userHasInteracted, setUserHasInteracted] = useState(false);
// Calculate total tools count
const totalTools = Object.values(toolCategories).reduce((sum, category) => {
if ('tools' in category) {
return sum + category.tools.length;
} else if ('subCategories' in category) {
return sum + Object.values(category.subCategories).reduce((subSum, subCat) => subSum + subCat.tools.length, 0);
}
return sum;
}, 0);
// Ripple effect for chips
const startRippleEffect = useCallback(() => {
const categoryEntries = Object.entries(toolCategories).slice(0, 5);
categoryEntries.forEach((_, index) => {
setTimeout(() => {
setRippleIndex(index);
// Reset ripple after animation
setTimeout(() => setRippleIndex(-1), 1000);
}, index * 200); // 200ms delay between each chip
});
}, [toolCategories]);
// Check if we should show the animation on mount (only once)
useEffect(() => {
if (shouldShowAnimation() && collapsed && !userHasInteracted) {
setShouldAutoExpand(true);
setIsAnimating(true);
markAnimationShown();
}
}, []); // Empty dependency array - only run once on mount
// Handle auto-expand animation
useEffect(() => {
if (shouldAutoExpand && collapsed && !userHasInteracted) {
// Auto-expand after a short delay
const expandTimer = setTimeout(() => {
onToggleCollapse(); // Expand the sidebar
}, 500);
// Start ripple effect after sidebar is expanded
const rippleTimer = setTimeout(() => {
startRippleEffect();
}, 1000);
// Auto-collapse after 2 seconds
const collapseTimer = setTimeout(() => {
onToggleCollapse(); // Collapse the sidebar
setIsAnimating(false);
setShouldAutoExpand(false);
}, 3000);
return () => {
clearTimeout(expandTimer);
clearTimeout(rippleTimer);
clearTimeout(collapseTimer);
};
}
}, [shouldAutoExpand, collapsed, onToggleCollapse, startRippleEffect, userHasInteracted]);
return (
<motion.div
initial={{ x: -300, opacity: 0 }}
animate={{
x: 0,
opacity: 1,
...(isAnimating && {
scale: [1, 1.02, 1],
boxShadow: [
'0 8px 32px rgba(0,0,0,0.1)',
'0 12px 40px rgba(74, 222, 128, 0.2)',
'0 8px 32px rgba(0,0,0,0.1)'
]
})
}}
transition={{
duration: 0.3,
...(isAnimating && {
scale: { duration: 2, ease: 'easeInOut' },
boxShadow: { duration: 2, ease: 'easeInOut' }
})
}}
>
<Paper
elevation={0}
sx={{
width: '100%',
height: 'fit-content',
minHeight: '400px',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
overflow: 'hidden',
boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
}}
>
{/* Header */}
<Box
sx={{
p: collapsed ? 1 : 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid rgba(255,255,255,0.1)',
minHeight: 56,
background: 'linear-gradient(90deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.2) 50%, transparent 100%)',
}
}}
>
{/* Animation indicator */}
{isAnimating && !collapsed && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
style={{
position: 'absolute',
top: -8,
right: -8,
width: 16,
height: 16,
borderRadius: '50%',
background: 'linear-gradient(45deg, #4ade80, #22c55e)',
boxShadow: '0 0 10px rgba(74, 222, 128, 0.6)',
zIndex: 10
}}
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
border: '2px solid transparent',
borderTop: '2px solid rgba(255,255,255,0.8)',
}}
/>
</motion.div>
)}
{!collapsed && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box sx={{
width: 8,
height: 8,
borderRadius: '50%',
background: 'linear-gradient(45deg, #4ade80, #22c55e)',
boxShadow: '0 0 8px rgba(74, 222, 128, 0.4)'
}} />
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
Tools
</Typography>
</Box>
)}
<Tooltip title={collapsed ? "Expand sidebar" : "Collapse sidebar"}>
<IconButton
size="small"
onClick={() => {
setUserHasInteracted(true);
onToggleCollapse();
}}
sx={{
color: 'rgba(255,255,255,0.7)',
backgroundColor: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
'&:hover': {
color: '#ffffff',
backgroundColor: 'rgba(255,255,255,0.1)',
transform: 'scale(1.05)'
},
transition: 'all 0.2s ease-in-out'
}}
>
{collapsed ? <ChevronRight size={16} /> : <ChevronLeft size={16} />}
</IconButton>
</Tooltip>
</Box>
{/* Content */}
<Box sx={{ p: collapsed ? 1 : 2, height: 'calc(100% - 56px)', overflow: 'auto' }}>
{!collapsed ? (
<>
{/* Search Section */}
<Box sx={{
mb: 2,
p: 2,
backgroundColor: 'rgba(255,255,255,0.03)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.05)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%)',
}
}}>
<Typography variant="subtitle2" sx={{
mb: 1,
color: 'rgba(255,255,255,0.9)',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: 1
}}>
<Search size={16} color="rgba(255,255,255,0.7)" />
Search Tools
</Typography>
<SearchFilter
searchQuery={searchQuery}
onSearchChange={onSearchChange}
onClearSearch={onClearSearch}
selectedCategory={selectedCategory}
onCategoryChange={onCategoryChange}
selectedSubCategory={selectedSubCategory}
onSubCategoryChange={onSubCategoryChange}
toolCategories={toolCategories}
theme={theme}
onCategoryClick={onCategoryClick}
compact={true}
/>
</Box>
<Divider sx={{ my: 2, borderColor: 'rgba(255,255,255,0.1)' }} />
{/* Quick Stats */}
<Box sx={{
mb: 2,
p: 2,
backgroundColor: 'rgba(255,255,255,0.03)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.05)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%)',
}
}}>
<Typography variant="subtitle2" sx={{
mb: 1,
color: 'rgba(255,255,255,0.9)',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: 1
}}>
<Activity size={16} color="rgba(255,255,255,0.7)" />
Quick Stats
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Total Tools
</Typography>
<Chip
label={totalTools}
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80',
border: '1px solid rgba(74, 222, 128, 0.3)'
}}
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Categories
</Typography>
<Chip
label={Object.keys(toolCategories).length}
size="small"
sx={{
backgroundColor: 'rgba(59, 130, 246, 0.2)',
color: '#3b82f6',
border: '1px solid rgba(59, 130, 246, 0.3)'
}}
/>
</Box>
</Box>
</Box>
<Divider sx={{ my: 2, borderColor: 'rgba(255,255,255,0.1)' }} />
{/* Category Quick Access */}
<Box sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.03)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.05)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%)',
}
}}>
<Typography variant="subtitle2" sx={{
mb: 1,
color: 'rgba(255,255,255,0.9)',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: 1
}}>
<Zap size={16} color="rgba(255,255,255,0.7)" />
Quick Access
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{Object.entries(toolCategories).slice(0, 5).map(([categoryId, category], index) => {
const toolCount = 'tools' in category
? category.tools.length
: Object.values(category.subCategories).reduce((sum, subCat) => sum + subCat.tools.length, 0);
const isRippling = rippleIndex === index;
return (
<Tooltip key={categoryId} title={`${categoryId} (${toolCount} tools)`}>
<motion.div
animate={isRippling ? {
scale: [1, 1.05, 1],
boxShadow: [
'0 0 0 rgba(74, 222, 128, 0)',
'0 0 20px rgba(74, 222, 128, 0.6)',
'0 0 0 rgba(74, 222, 128, 0)'
]
} : {}}
transition={{ duration: 1, ease: 'easeInOut' }}
>
<Chip
label={`${categoryId} (${toolCount})`}
size="small"
onClick={() => {
setUserHasInteracted(true);
onCategoryClick(categoryId, category);
}}
sx={{
backgroundColor: selectedCategory === categoryId
? 'rgba(74, 222, 128, 0.2)'
: isRippling
? 'rgba(74, 222, 128, 0.15)'
: 'rgba(255,255,255,0.05)',
color: selectedCategory === categoryId || isRippling ? '#4ade80' : '#ffffff',
border: selectedCategory === categoryId
? '1px solid rgba(74, 222, 128, 0.3)'
: isRippling
? '1px solid rgba(74, 222, 128, 0.4)'
: '1px solid rgba(255,255,255,0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
position: 'relative',
overflow: 'hidden',
'&::before': isRippling ? {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent)',
animation: 'shimmer 1s ease-in-out',
'@keyframes shimmer': {
'0%': { left: '-100%' },
'100%': { left: '100%' }
}
} : {},
'&:hover': {
backgroundColor: selectedCategory === categoryId
? 'rgba(74, 222, 128, 0.3)'
: 'rgba(255,255,255,0.15)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}
}}
/>
</motion.div>
</Tooltip>
);
})}
</Box>
</Box>
</>
) : (
/* Collapsed State - Enhanced Icons with Depth */
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, pt: 2 }}>
<Tooltip title="Search Tools">
<motion.div
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<IconButton
size="small"
sx={{
color: searchQuery ? '#4ade80' : 'rgba(255,255,255,0.8)',
backgroundColor: searchQuery
? 'rgba(74, 222, 128, 0.15)'
: 'rgba(255,255,255,0.08)',
border: searchQuery
? '2px solid rgba(74, 222, 128, 0.4)'
: '2px solid rgba(255,255,255,0.15)',
borderRadius: '12px',
width: 40,
height: 40,
boxShadow: searchQuery
? '0 8px 25px rgba(74, 222, 128, 0.3), 0 0 20px rgba(74, 222, 128, 0.2), inset 0 1px 0 rgba(255,255,255,0.2)'
: '0 6px 20px rgba(0,0,0,0.15), 0 0 15px rgba(255,255,255,0.1), inset 0 1px 0 rgba(255,255,255,0.1)',
backdropFilter: 'blur(10px)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: searchQuery
? 'linear-gradient(135deg, rgba(74, 222, 128, 0.1) 0%, rgba(34, 197, 94, 0.1) 100%)'
: 'linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
borderRadius: '10px',
zIndex: -1
},
'&:hover': {
color: '#ffffff',
backgroundColor: searchQuery
? 'rgba(74, 222, 128, 0.25)'
: 'rgba(255,255,255,0.15)',
boxShadow: searchQuery
? '0 12px 35px rgba(74, 222, 128, 0.4), 0 0 30px rgba(74, 222, 128, 0.3), inset 0 1px 0 rgba(255,255,255,0.3)'
: '0 10px 30px rgba(0,0,0,0.2), 0 0 25px rgba(255,255,255,0.15), inset 0 1px 0 rgba(255,255,255,0.2)',
transform: 'translateY(-2px)'
}
}}
>
<Search size={18} />
</IconButton>
</motion.div>
</Tooltip>
<Tooltip title="Filter Categories">
<motion.div
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<IconButton
size="small"
sx={{
color: selectedCategory ? '#3b82f6' : 'rgba(255,255,255,0.8)',
backgroundColor: selectedCategory
? 'rgba(59, 130, 246, 0.15)'
: 'rgba(255,255,255,0.08)',
border: selectedCategory
? '2px solid rgba(59, 130, 246, 0.4)'
: '2px solid rgba(255,255,255,0.15)',
borderRadius: '12px',
width: 40,
height: 40,
boxShadow: selectedCategory
? '0 8px 25px rgba(59, 130, 246, 0.3), 0 0 20px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255,255,255,0.2)'
: '0 6px 20px rgba(0,0,0,0.15), 0 0 15px rgba(255,255,255,0.1), inset 0 1px 0 rgba(255,255,255,0.1)',
backdropFilter: 'blur(10px)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: selectedCategory
? 'linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(37, 99, 235, 0.1) 100%)'
: 'linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
borderRadius: '10px',
zIndex: -1
},
'&:hover': {
color: '#ffffff',
backgroundColor: selectedCategory
? 'rgba(59, 130, 246, 0.25)'
: 'rgba(255,255,255,0.15)',
boxShadow: selectedCategory
? '0 12px 35px rgba(59, 130, 246, 0.4), 0 0 30px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255,255,255,0.3)'
: '0 10px 30px rgba(0,0,0,0.2), 0 0 25px rgba(255,255,255,0.15), inset 0 1px 0 rgba(255,255,255,0.2)',
transform: 'translateY(-2px)'
}
}}
>
<Filter size={18} />
</IconButton>
</motion.div>
</Tooltip>
<Divider sx={{ width: '100%', borderColor: 'rgba(255,255,255,0.1)' }} />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{Object.entries(toolCategories).slice(0, 4).map(([categoryId, category]) => {
const toolCount = 'tools' in category
? category.tools.length
: Object.values(category.subCategories).reduce((sum, subCat) => sum + subCat.tools.length, 0);
const isSelected = selectedCategory === categoryId;
return (
<Tooltip key={categoryId} title={`${categoryId} (${toolCount})`}>
<motion.div
whileHover={{ scale: 1.15, y: -3 }}
whileTap={{ scale: 0.9 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<Box
sx={{
width: 36,
height: 36,
borderRadius: '50%',
backgroundColor: isSelected
? 'rgba(74, 222, 128, 0.2)'
: 'rgba(255,255,255,0.08)',
border: isSelected
? '2px solid rgba(74, 222, 128, 0.5)'
: '2px solid rgba(255,255,255,0.2)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: isSelected
? '0 8px 25px rgba(74, 222, 128, 0.3), 0 0 20px rgba(74, 222, 128, 0.2), inset 0 1px 0 rgba(255,255,255,0.2)'
: '0 6px 20px rgba(0,0,0,0.15), 0 0 15px rgba(255,255,255,0.1), inset 0 1px 0 rgba(255,255,255,0.1)',
backdropFilter: 'blur(10px)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: isSelected
? 'linear-gradient(135deg, rgba(74, 222, 128, 0.1) 0%, rgba(34, 197, 94, 0.1) 100%)'
: 'linear-gradient(135deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.02) 100%)',
borderRadius: '50%',
zIndex: -1
},
'&:hover': {
backgroundColor: isSelected
? 'rgba(74, 222, 128, 0.3)'
: 'rgba(255,255,255,0.15)',
boxShadow: isSelected
? '0 12px 35px rgba(74, 222, 128, 0.4), 0 0 30px rgba(74, 222, 128, 0.3), inset 0 1px 0 rgba(255,255,255,0.3)'
: '0 10px 30px rgba(0,0,0,0.2), 0 0 25px rgba(255,255,255,0.15), inset 0 1px 0 rgba(255,255,255,0.2)',
transform: 'translateY(-3px)'
}
}}
onClick={() => {
setUserHasInteracted(true);
onCategoryClick(categoryId, category);
}}
>
<Typography
variant="caption"
sx={{
color: isSelected ? '#4ade80' : '#ffffff',
fontWeight: 'bold',
fontSize: '0.75rem',
textShadow: '0 1px 2px rgba(0,0,0,0.3)'
}}
>
{categoryId.charAt(0).toUpperCase()}
</Typography>
</Box>
</motion.div>
</Tooltip>
);
})}
</Box>
</Box>
)}
</Box>
</Paper>
</motion.div>
);
};
export default CompactSidebar;

View File

@@ -0,0 +1,279 @@
import React from 'react';
import {
Box,
Typography,
Button,
useTheme,
useMediaQuery
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
PlayArrow,
TrendingUp,
Rocket,
ArrowRight,
Star
} from '@mui/icons-material';
interface WorkflowHeroSectionProps {
onStartWorkflow: () => void;
isWorkflowActive: boolean;
isLoading: boolean;
}
const WorkflowHeroSection: React.FC<WorkflowHeroSectionProps> = ({
onStartWorkflow,
isWorkflowActive,
isLoading
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// Show hero section only when workflow is not started, not in progress, and not completed
const shouldShowHero = !isWorkflowActive;
return (
<AnimatePresence>
{shouldShowHero && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
{/* Backdrop Overlay - Only over pillars section */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.6)',
backdropFilter: 'blur(6px)',
zIndex: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 2, // Match the parent container's border radius
}}
>
{/* Hero Content */}
<Box
sx={{
textAlign: 'center',
maxWidth: isMobile ? '90%' : '500px',
px: 3,
py: 4,
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(20px)',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.2)',
boxShadow: '0 15px 30px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'linear-gradient(45deg, rgba(255,107,53,0.1) 0%, rgba(255,140,66,0.1) 50%, rgba(255,107,53,0.1) 100%)',
backgroundSize: '200% 200%',
animation: 'gradientShift 6s ease-in-out infinite',
zIndex: -1,
},
'@keyframes gradientShift': {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
}}
>
{/* Floating Sparkles */}
<Box
sx={{
position: 'absolute',
top: 20,
right: 20,
animation: 'float 3s ease-in-out infinite',
'@keyframes float': {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-10px)' },
},
}}
>
<Star sx={{ color: 'rgba(255,255,255,0.6)', fontSize: 24 }} />
</Box>
<Box
sx={{
position: 'absolute',
bottom: 20,
left: 20,
animation: 'float 3s ease-in-out infinite 1.5s',
'@keyframes float': {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-10px)' },
},
}}
>
<TrendingUp sx={{ color: 'rgba(255,255,255,0.6)', fontSize: 24 }} />
</Box>
{/* Main Content */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
>
{/* Icon */}
<Box sx={{ mb: 2 }}>
<motion.div
animate={{
rotate: [0, 5, -5, 0],
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
repeatType: "reverse"
}}
>
<Rocket
sx={{
fontSize: isMobile ? 40 : 48,
color: '#FF6B35',
filter: 'drop-shadow(0 4px 8px rgba(255,107,53,0.3))'
}}
/>
</motion.div>
</Box>
{/* Main Heading */}
<Typography
variant={isMobile ? "h5" : "h4"}
sx={{
fontWeight: 800,
color: '#ffffff',
mb: 1.5,
textShadow: '0 4px 8px rgba(0,0,0,0.3)',
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}
>
Grow Your Business Now
</Typography>
{/* Supporting Text */}
<Typography
variant={isMobile ? "body2" : "body1"}
sx={{
color: 'rgba(255,255,255,0.9)',
mb: 3,
lineHeight: 1.5,
maxWidth: '400px',
mx: 'auto',
textShadow: '0 2px 4px rgba(0,0,0,0.3)',
}}
>
Start your personalized content workflow and watch your digital marketing transform.
Our AI-powered system will guide you through every step of your content journey.
</Typography>
{/* CTA Button */}
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
variant="contained"
size="large"
startIcon={<PlayArrow />}
endIcon={<ArrowRight />}
onClick={onStartWorkflow}
disabled={isLoading}
sx={{
background: 'linear-gradient(135deg, #FF6B35 0%, #E55A2B 100%)',
border: '2px solid transparent',
borderRadius: 2,
px: 4,
py: 1.5,
fontSize: isMobile ? '0.9rem' : '1rem',
fontWeight: 700,
textTransform: 'none',
boxShadow: '0 6px 24px rgba(255,107,53,0.4), 0 0 0 1px rgba(255,255,255,0.2)',
position: 'relative',
overflow: 'hidden',
'&:hover': {
background: 'linear-gradient(135deg, #E55A2B 0%, #D1491F 100%)',
boxShadow: '0 12px 40px rgba(255,107,53,0.6), 0 0 0 1px rgba(255,255,255,0.3)',
transform: 'translateY(-2px)',
},
'&:disabled': {
background: 'rgba(255,255,255,0.1)',
color: 'rgba(255,255,255,0.5)',
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
animation: 'shimmer 2.5s infinite',
zIndex: 1,
},
'&::after': {
content: '""',
position: 'absolute',
top: -4,
left: -4,
right: -4,
bottom: -4,
background: 'linear-gradient(45deg, #FF6B35, #FF8C42, #FF6B35, #FF8C42)',
backgroundSize: '400% 400%',
borderRadius: 'inherit',
zIndex: -1,
animation: 'borderGlow 3s ease-in-out infinite',
},
'@keyframes shimmer': {
'0%': { left: '-100%' },
'100%': { left: '100%' },
},
'@keyframes borderGlow': {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
{isLoading ? 'Starting...' : '🚀 Start Your Journey'}
</Button>
</motion.div>
{/* Additional Info */}
<Typography
variant="caption"
sx={{
color: 'rgba(255,255,255,0.7)',
mt: 2,
display: 'block',
textShadow: '0 1px 2px rgba(0,0,0,0.3)',
}}
>
Personalized workflow 🎯 AI-powered guidance 📈 Business growth
</Typography>
</motion.div>
</Box>
</Box>
</motion.div>
)}
</AnimatePresence>
);
};
export default WorkflowHeroSection;

View File

@@ -0,0 +1,350 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Grid,
Card,
CardContent,
Typography,
Alert,
CircularProgress,
useTheme,
useMediaQuery,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
DollarSign,
TrendingUp,
AlertTriangle,
Activity,
Zap,
BarChart3,
PieChart,
Clock
} from 'lucide-react';
// Services
import { billingService } from '../../services/billingService';
import { monitoringService } from '../../services/monitoringService';
// Types
import { DashboardData, UsageStats } from '../../types/billing';
import { SystemHealth } from '../../types/monitoring';
// Components (we'll create these next)
import BillingOverview from './BillingOverview';
import CostBreakdown from './CostBreakdown';
import UsageTrends from './UsageTrends';
import SystemHealthIndicator from '../monitoring/SystemHealthIndicator';
import UsageAlerts from './UsageAlerts';
// Animation variants
const containerVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
staggerChildren: 0.1
}
}
};
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4 }
}
};
const BillingDashboard: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
// State management
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Fetch dashboard data
const fetchDashboardData = async () => {
try {
setLoading(true);
setError(null);
console.log('🔍 [DASHBOARD DEBUG] Starting data fetch...');
// Fetch billing and monitoring data in parallel
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(),
monitoringService.getSystemHealth()
]);
console.log('🔍 [DASHBOARD DEBUG] Received billing data:', billingData);
console.log('🔍 [DASHBOARD DEBUG] Received health data:', healthData);
console.log('🔍 [DASHBOARD DEBUG] Billing data current_usage:', billingData?.current_usage);
console.log('🔍 [DASHBOARD DEBUG] Billing data summary:', billingData?.summary);
console.log('🔍 [DASHBOARD DEBUG] Billing data trends:', billingData?.trends);
setDashboardData(billingData);
setSystemHealth(healthData);
setLastUpdated(new Date());
console.log('✅ [DASHBOARD DEBUG] Data set successfully');
} catch (err) {
console.error('❌ [DASHBOARD DEBUG] Error fetching dashboard data:', err);
setError(err instanceof Error ? err.message : 'Failed to load dashboard data');
} finally {
setLoading(false);
}
};
// Initial data fetch
useEffect(() => {
fetchDashboardData();
}, []);
// Auto-refresh every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
fetchDashboardData();
}, 30000);
return () => clearInterval(interval);
}, []);
// Loading state
if (loading && !dashboardData) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '400px',
flexDirection: 'column',
gap: 2
}}
>
<CircularProgress size={48} />
<Typography variant="body1" color="text.secondary">
Loading billing dashboard...
</Typography>
</Box>
);
}
// Error state
if (error && !dashboardData) {
return (
<Box sx={{ p: 3 }}>
<Alert
severity="error"
action={
<motion.button
onClick={fetchDashboardData}
style={{
background: 'none',
border: 'none',
color: 'inherit',
cursor: 'pointer',
textDecoration: 'underline'
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Retry
</motion.button>
}
>
{error}
</Alert>
</Box>
);
}
if (!dashboardData) {
return null;
}
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
<Container maxWidth="xl" sx={{ py: 4 }}>
{/* Section Header */}
<motion.div variants={cardVariants}>
<Box sx={{ mb: 4, textAlign: 'center' }}>
<Typography
variant="h4"
component="h2"
sx={{
fontWeight: 'bold',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
backgroundClip: 'text',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
mb: 1
}}
>
💰 Billing & Usage Dashboard
</Typography>
<Typography variant="body1" color="text.secondary">
Monitor your API usage, costs, and system performance in real-time
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
Last updated: {lastUpdated.toLocaleTimeString()}
</Typography>
</Box>
</motion.div>
{/* Main Dashboard Grid */}
<Grid container spacing={3}>
{/* Top Row - Overview Cards */}
<Grid item xs={12} md={4}>
<motion.div variants={cardVariants}>
<BillingOverview
usageStats={dashboardData.current_usage}
onRefresh={fetchDashboardData}
/>
</motion.div>
</Grid>
<Grid item xs={12} md={4}>
<motion.div variants={cardVariants}>
<SystemHealthIndicator
systemHealth={systemHealth}
onRefresh={fetchDashboardData}
/>
</motion.div>
</Grid>
<Grid item xs={12} md={4}>
<motion.div variants={cardVariants}>
<UsageAlerts
alerts={dashboardData.alerts}
onMarkRead={billingService.markAlertRead}
/>
</motion.div>
</Grid>
{/* Middle Row - Cost Breakdown */}
<Grid item xs={12} lg={6}>
<motion.div variants={cardVariants}>
<CostBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</motion.div>
</Grid>
{/* Middle Row - Usage Trends */}
<Grid item xs={12} lg={6}>
<motion.div variants={cardVariants}>
<UsageTrends
trends={dashboardData.trends}
projections={dashboardData.projections}
/>
</motion.div>
</Grid>
{/* Bottom Row - Detailed Metrics */}
<Grid item xs={12}>
<motion.div variants={cardVariants}>
<Card
sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}
>
<CardContent>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<BarChart3 size={20} />
Detailed Usage Metrics
</Typography>
<Grid container spacing={3}>
{/* Usage Summary */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" sx={{ color: 'primary.main', fontWeight: 'bold' }}>
{dashboardData.current_usage.total_calls.toLocaleString()}
</Typography>
<Typography variant="body2" color="text.secondary">
Total API Calls
</Typography>
<Typography variant="caption" color="text.secondary">
This month
</Typography>
</Box>
</Grid>
{/* Token Usage */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" sx={{ color: 'secondary.main', fontWeight: 'bold' }}>
{(dashboardData.current_usage.total_tokens / 1000).toFixed(1)}k
</Typography>
<Typography variant="body2" color="text.secondary">
Tokens Used
</Typography>
<Typography variant="caption" color="text.secondary">
This month
</Typography>
</Box>
</Grid>
{/* Average Response Time */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" sx={{ color: 'warning.main', fontWeight: 'bold' }}>
{dashboardData.current_usage.avg_response_time.toFixed(0)}ms
</Typography>
<Typography variant="body2" color="text.secondary">
Avg Response Time
</Typography>
<Typography variant="caption" color="text.secondary">
Last 24 hours
</Typography>
</Box>
</Grid>
{/* Error Rate */}
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography
variant="h4"
sx={{
color: dashboardData.current_usage.error_rate > 5 ? 'error.main' : 'success.main',
fontWeight: 'bold'
}}
>
{dashboardData.current_usage.error_rate.toFixed(2)}%
</Typography>
<Typography variant="body2" color="text.secondary">
Error Rate
</Typography>
<Typography variant="caption" color="text.secondary">
Last 24 hours
</Typography>
</Box>
</Grid>
</Grid>
</CardContent>
</Card>
</motion.div>
</Grid>
</Grid>
</Container>
</motion.div>
);
};
export default BillingDashboard;

View File

@@ -0,0 +1,286 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
LinearProgress,
Chip,
IconButton,
Tooltip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
DollarSign,
TrendingUp,
RefreshCw,
AlertTriangle,
CheckCircle,
XCircle,
Info
} from 'lucide-react';
// Types
import { UsageStats } from '../../types/billing';
// Utils
import {
formatCurrency,
formatNumber,
formatPercentage,
getUsageStatusColor,
getUsageStatusIcon,
calculateUsagePercentage
} from '../../services/billingService';
interface BillingOverviewProps {
usageStats: UsageStats;
onRefresh: () => void;
}
const BillingOverview: React.FC<BillingOverviewProps> = ({
usageStats,
onRefresh
}) => {
// Debug logs removed to reduce console noise
const costUsagePercentage = calculateUsagePercentage(
usageStats.total_cost,
usageStats.limits.limits.monthly_cost || 1
);
// Debug logs removed to reduce console noise
const getStatusChip = () => {
const status = usageStats.usage_status;
const color = getUsageStatusColor(status);
const icon = getUsageStatusIcon(status);
let chipColor: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
if (status === 'active') chipColor = 'success';
else if (status === 'warning') chipColor = 'warning';
else if (status === 'limit_reached') chipColor = 'error';
return (
<Chip
icon={<span>{icon}</span>}
label={status.charAt(0).toUpperCase() + status.slice(1).replace('_', ' ')}
color={chipColor}
size="small"
sx={{ fontWeight: 'bold' }}
/>
);
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
<DollarSign size={20} />
Billing Overview
</Typography>
<Tooltip title="View your current billing status, usage metrics, and subscription plan details">
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
<Tooltip title="Refresh data">
<IconButton
size="small"
onClick={onRefresh}
sx={{
color: 'text.secondary',
'&:hover': { color: 'primary.main' }
}}
>
<RefreshCw size={16} />
</IconButton>
</Tooltip>
</Box>
{/* Status Chip */}
<Box sx={{ mb: 3 }}>
{getStatusChip()}
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Current Cost */}
<Box sx={{ mb: 3, textAlign: 'center' }}>
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Typography
variant="h3"
sx={{
fontWeight: 'bold',
color: '#ffffff',
textShadow: '0 2px 4px rgba(0,0,0,0.3)',
mb: 1
}}
>
{formatCurrency(usageStats.total_cost)}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
Total Cost This Month
</Typography>
</motion.div>
</Box>
{/* Usage Metrics */}
<Box sx={{ mb: 3 }}>
<Tooltip title="Total number of API requests made this billing period">
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
API Calls
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(usageStats.total_calls)}
</Typography>
</Box>
</Tooltip>
<Tooltip title="Total tokens processed across all API providers (input + output tokens)">
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Tokens Used
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(usageStats.total_tokens)}
</Typography>
</Box>
</Tooltip>
<Tooltip title="Average response time for API requests in the last 24 hours">
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Avg Response Time
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{usageStats.avg_response_time.toFixed(0)}ms
</Typography>
</Box>
</Tooltip>
</Box>
{/* Cost Usage Progress */}
{usageStats.limits.limits.monthly_cost > 0 && (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Monthly Cost Limit
</Typography>
<Typography variant="body2" fontWeight="bold">
{formatPercentage(costUsagePercentage)}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={Math.min(costUsagePercentage, 100)}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
backgroundColor: costUsagePercentage > 80 ? '#ef4444' :
costUsagePercentage > 60 ? '#f59e0b' : '#22c55e',
borderRadius: 4,
}
}}
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{formatCurrency(usageStats.total_cost)} of {formatCurrency(usageStats.limits.limits.monthly_cost)} limit
</Typography>
</Box>
)}
{/* Plan Information */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ mb: 1, color: 'rgba(255,255,255,0.8)' }}>
Current Plan
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', mb: 1, color: '#ffffff' }}>
{usageStats.limits.plan_name}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{usageStats.limits.tier.charAt(0).toUpperCase() + usageStats.limits.tier.slice(1)} Tier
</Typography>
</Box>
{/* Quick Stats */}
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'space-around' }}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{usageStats.usage_percentages.gemini_calls.toFixed(0)}%
</Typography>
<Typography variant="caption" color="text.secondary">
Gemini Usage
</Typography>
</Box>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'secondary.main' }}>
{usageStats.error_rate.toFixed(1)}%
</Typography>
<Typography variant="caption" color="text.secondary">
Error Rate
</Typography>
</Box>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default BillingOverview;

View File

@@ -0,0 +1,614 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
LinearProgress,
IconButton,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
AlertTriangle,
CheckCircle,
RefreshCw
} from 'lucide-react';
// Types
import { DashboardData } from '../../types/billing';
import { SystemHealth } from '../../types/monitoring';
// Services
import { billingService } from '../../services/billingService';
import { monitoringService } from '../../services/monitoringService';
import { onApiEvent } from '../../utils/apiEvents';
// Components
import ComprehensiveAPIBreakdown from './ComprehensiveAPIBreakdown';
interface CompactBillingDashboardProps {
userId?: string;
}
const CompactBillingDashboard: React.FC<CompactBillingDashboardProps> = ({ userId }) => {
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(userId),
monitoringService.getSystemHealth()
]);
setDashboardData(billingData);
setSystemHealth(healthData);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch data');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [userId]);
// Event-driven refresh
useEffect(() => {
const lastRefreshRef = { current: 0 } as { current: number };
const MIN_REFRESH_INTERVAL_MS = 4000;
const unsubscribe = onApiEvent((detail) => {
// Only react to non-billing/monitoring events to avoid feedback loops
if (detail.source && detail.source !== 'other') return;
const now = Date.now();
if (now - lastRefreshRef.current < MIN_REFRESH_INTERVAL_MS) return;
lastRefreshRef.current = now;
fetchData();
});
return unsubscribe;
}, []);
const formatCurrency = (amount: number) => `$${amount.toFixed(4)}`;
const formatNumber = (num: number) => num.toLocaleString();
const formatPercentage = (num: number) => `${num.toFixed(1)}%`;
if (loading && !dashboardData) {
return (
<Card sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography sx={{ color: 'rgba(255,255,255,0.8)' }}>Loading billing data...</Typography>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3
}}>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography sx={{ color: '#ff6b6b' }}>Error: {error}</Typography>
<IconButton onClick={fetchData} sx={{ mt: 1 }}>
<RefreshCw size={16} />
</IconButton>
</CardContent>
</Card>
);
}
if (!dashboardData) return null;
const { current_usage, trends, limits, alerts } = dashboardData;
const activeProviders = Object.entries(current_usage.provider_breakdown)
.filter(([_, data]) => data.cost > 0);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card
sx={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.08) 100%)',
backdropFilter: 'blur(15px)',
border: '1px solid rgba(255,255,255,0.15)',
borderRadius: 4,
position: 'relative',
overflow: 'hidden',
boxShadow: '0 8px 32px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.1)',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '1px',
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent)',
zIndex: 1
}
}}
>
{/* Header - Removed to save space */}
<CardContent sx={{ pt: 2 }}>
{/* Compact Overview */}
<Grid container spacing={2} sx={{ mb: 2 }}>
{/* Total Cost */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
Monthly API Usage Cost
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Total spending across all AI providers this month
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Includes: Gemini, OpenAI, Anthropic, Mistral
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: 'linear-gradient(135deg, rgba(74, 222, 128, 0.12) 0%, rgba(34, 197, 94, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(74, 222, 128, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(74, 222, 128, 0.2)',
border: '1px solid rgba(74, 222, 128, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #4ade80, #22c55e)',
zIndex: 1
}
}}>
<Typography variant="h5" sx={{
fontWeight: 800,
color: '#ffffff',
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{formatCurrency(current_usage.total_cost)}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
Total Cost
</Typography>
</Box>
</Tooltip>
</Grid>
{/* API Calls */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
API Request Volume
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Total number of AI API requests made this month
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Each request generates content, analyzes data, or processes information
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: 'linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(37, 99, 235, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(59, 130, 246, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(59, 130, 246, 0.2)',
border: '1px solid rgba(59, 130, 246, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #3b82f6, #2563eb)',
zIndex: 1
}
}}>
<Typography variant="h5" sx={{
fontWeight: 800,
color: '#ffffff',
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{formatNumber(current_usage.total_calls)}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
API Calls
</Typography>
</Box>
</Tooltip>
</Grid>
{/* Tokens */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
AI Processing Units
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Total tokens processed by AI models this month
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
Tokens represent words, characters, and data processed by AI
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.12) 0%, rgba(147, 51, 234, 0.08) 100%)',
borderRadius: 3,
border: '1px solid rgba(168, 85, 247, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: '0 8px 25px rgba(168, 85, 247, 0.2)',
border: '1px solid rgba(168, 85, 247, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #a855f7, #9333ea)',
zIndex: 1
}
}}>
<Typography variant="h5" sx={{
fontWeight: 800,
color: '#ffffff',
textShadow: '0 2px 8px rgba(0,0,0,0.4)',
mb: 0.5
}}>
{(current_usage.total_tokens / 1000).toFixed(1)}k
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
Tokens
</Typography>
</Box>
</Tooltip>
</Grid>
{/* System Health */}
<Grid item xs={6} sm={3}>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
System Performance Status
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Real-time monitoring of API services and system performance
</Typography>
<Typography variant="caption" sx={{ opacity: 0.7, mt: 1, display: 'block' }}>
{systemHealth?.status === 'healthy'
? 'All systems operational and responding normally'
: 'Some services may be experiencing issues'
}
</Typography>
</Box>
}
arrow
placement="top"
>
<Box sx={{
textAlign: 'center',
p: 2.5,
background: systemHealth?.status === 'healthy'
? 'linear-gradient(135deg, rgba(34, 197, 94, 0.12) 0%, rgba(22, 163, 74, 0.08) 100%)'
: 'linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.08) 100%)',
borderRadius: 3,
border: systemHealth?.status === 'healthy'
? '1px solid rgba(34, 197, 94, 0.25)'
: '1px solid rgba(239, 68, 68, 0.25)',
position: 'relative',
overflow: 'hidden',
cursor: 'help',
transition: 'all 0.3s ease',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: systemHealth?.status === 'healthy'
? '0 8px 25px rgba(34, 197, 94, 0.2)'
: '0 8px 25px rgba(239, 68, 68, 0.2)',
border: systemHealth?.status === 'healthy'
? '1px solid rgba(34, 197, 94, 0.4)'
: '1px solid rgba(239, 68, 68, 0.4)'
},
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: systemHealth?.status === 'healthy'
? 'linear-gradient(90deg, #22c55e, #16a34a)'
: 'linear-gradient(90deg, #ef4444, #dc2626)',
zIndex: 1
}
}}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, mb: 0.5 }}>
<CheckCircle size={18} color={systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b'} />
<Typography variant="body1" sx={{
color: systemHealth?.status === 'healthy' ? '#4ade80' : '#ff6b6b',
fontWeight: 700,
textTransform: 'capitalize',
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
}}>
{systemHealth?.status || 'Unknown'}
</Typography>
</Box>
<Typography variant="body2" sx={{
color: 'rgba(255,255,255,0.9)',
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: '0.5px',
fontSize: '0.75rem'
}}>
System Health
</Typography>
</Box>
</Tooltip>
</Grid>
</Grid>
{/* Usage Progress */}
{limits.limits.monthly_cost > 0 && (
<Box sx={{
mb: 3,
p: 2.5,
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.04) 100%)',
borderRadius: 3,
border: '1px solid rgba(255,255,255,0.1)'
}}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box>
<Typography variant="subtitle2" sx={{
color: '#ffffff',
fontWeight: 600,
mb: 0.5
}}>
Monthly Budget Usage
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.7)',
display: 'block'
}}>
Track your AI spending against monthly limits
</Typography>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Typography variant="h6" sx={{
color: '#ffffff',
fontWeight: 'bold',
textShadow: '0 2px 4px rgba(0,0,0,0.3)'
}}>
{formatCurrency(current_usage.total_cost)}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.7)',
display: 'block'
}}>
of {formatCurrency(limits.limits.monthly_cost)}
</Typography>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={(current_usage.total_cost / limits.limits.monthly_cost) * 100}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
background: current_usage.total_cost / limits.limits.monthly_cost > 0.8
? 'linear-gradient(90deg, #ff6b6b, #ff5252)'
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
? 'linear-gradient(90deg, #ffa726, #ff9800)'
: 'linear-gradient(90deg, #4ade80, #22c55e)',
borderRadius: 4,
boxShadow: '0 2px 8px rgba(0,0,0,0.2)'
}
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 1 }}>
<Typography variant="caption" sx={{
color: current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? '#ff6b6b' : 'rgba(255,255,255,0.7)',
fontWeight: current_usage.total_cost / limits.limits.monthly_cost > 0.8 ? 600 : 400
}}>
{current_usage.total_cost / limits.limits.monthly_cost > 0.8
? '⚠️ Approaching limit'
: current_usage.total_cost / limits.limits.monthly_cost > 0.6
? '⚡ Moderate usage'
: '✅ Within budget'
}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.7)',
fontWeight: 500
}}>
{((current_usage.total_cost / limits.limits.monthly_cost) * 100).toFixed(1)}% used
</Typography>
</Box>
</Box>
)}
{/* Alerts */}
{alerts.length > 0 && (
<Box sx={{
mb: 3,
p: 2.5,
background: 'linear-gradient(135deg, rgba(255, 107, 107, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%)',
borderRadius: 3,
border: '1px solid rgba(255, 107, 107, 0.2)',
position: 'relative',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #ff6b6b, #ef4444)',
borderRadius: '3px 3px 0 0'
}
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<AlertTriangle size={18} color="#ff6b6b" />
<Typography variant="subtitle2" sx={{
fontWeight: 700,
color: '#ff6b6b',
textShadow: '0 1px 2px rgba(0,0,0,0.3)'
}}>
System Alerts ({alerts.length})
</Typography>
</Box>
<Typography variant="caption" sx={{
color: 'rgba(255,255,255,0.8)',
display: 'block',
mb: 2
}}>
Important notifications requiring your attention
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{alerts.slice(0, 3).map((alert) => (
<Tooltip
key={alert.id}
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{alert.title}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
{alert.message}
</Typography>
</Box>
}
arrow
placement="top"
>
<Chip
label={alert.title}
size="small"
icon={<AlertTriangle size={14} />}
sx={{
backgroundColor: 'rgba(255, 107, 107, 0.2)',
color: '#ff6b6b',
border: '1px solid rgba(255, 107, 107, 0.3)',
fontWeight: 500,
'&:hover': {
backgroundColor: 'rgba(255, 107, 107, 0.3)',
transform: 'translateY(-1px)'
},
transition: 'all 0.2s ease'
}}
/>
</Tooltip>
))}
{alerts.length > 3 && (
<Chip
label={`+${alerts.length - 3} more`}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
color: 'rgba(255,255,255,0.8)',
border: '1px solid rgba(255,255,255,0.2)',
fontWeight: 500
}}
/>
)}
</Box>
</Box>
)}
</CardContent>
</Card>
</motion.div>
);
};
export default CompactBillingDashboard;

View File

@@ -0,0 +1,414 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
Accordion,
AccordionSummary,
AccordionDetails,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { motion } from 'framer-motion';
import {
Info,
DollarSign,
Activity,
Zap,
Search,
Image,
Code,
Database,
Globe,
FileText,
BarChart3
} from 'lucide-react';
// Types
import { ProviderBreakdown } from '../../types/billing';
interface ComprehensiveAPIBreakdownProps {
providerBreakdown: ProviderBreakdown;
totalCost: number;
}
// Comprehensive API categories and their descriptions
const API_CATEGORIES = {
llm_models: {
title: 'Large Language Models',
description: 'AI models for text generation, analysis, and processing',
icon: <Code size={20} />,
apis: [
{
name: 'Gemini',
description: 'Google\'s advanced AI model for complex reasoning and coding',
models: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
pricing: 'From $0.10/1M tokens (Flash-Lite) to $15.00/1M tokens (Pro)',
use_cases: ['Content generation', 'Code analysis', 'Complex reasoning']
},
{
name: 'OpenAI',
description: 'GPT models for natural language processing and generation',
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],
pricing: 'From $0.15/1M tokens (GPT-4o Mini) to $10.00/1M tokens (GPT-4o)',
use_cases: ['Chat completion', 'Text analysis', 'Creative writing']
},
{
name: 'Anthropic',
description: 'Claude models for safe and helpful AI assistance',
models: ['claude-3.5-sonnet', 'claude-3-haiku', 'claude-3-opus'],
pricing: 'From $3.00/1M tokens (Sonnet) to $15.00/1M tokens (Opus)',
use_cases: ['Safe AI assistance', 'Long-form content', 'Analysis tasks']
},
{
name: 'Mistral',
description: 'European AI models for efficient text processing',
models: ['mistral-large', 'mistral-medium', 'mistral-small'],
pricing: 'From $2.00/1M tokens (Small) to $8.00/1M tokens (Large)',
use_cases: ['Multilingual support', 'Efficient processing', 'European compliance']
}
]
},
search_apis: {
title: 'Search & Research APIs',
description: 'APIs for web search, content discovery, and research',
icon: <Search size={20} />,
apis: [
{
name: 'Tavily',
description: 'AI-powered search for real-time information',
models: ['tavily-search'],
pricing: '$0.001 per search request',
use_cases: ['Real-time search', 'Fact checking', 'Research assistance']
},
{
name: 'Serper',
description: 'Google Search API for web results',
models: ['serper-search'],
pricing: '$0.001 per search request',
use_cases: ['Web search', 'SEO analysis', 'Content research']
},
{
name: 'Metaphor',
description: 'Advanced search and content discovery',
models: ['metaphor-search'],
pricing: '$0.003 per search request',
use_cases: ['Content discovery', 'Link analysis', 'Research automation']
}
]
},
content_processing: {
title: 'Content Processing APIs',
description: 'APIs for web scraping, content extraction, and processing',
icon: <FileText size={20} />,
apis: [
{
name: 'Firecrawl',
description: 'Web scraping and content extraction service',
models: ['firecrawl-extract', 'firecrawl-scrape'],
pricing: '$0.002 per page crawled',
use_cases: ['Web scraping', 'Content extraction', 'Data collection']
}
]
},
image_generation: {
title: 'Image Generation APIs',
description: 'APIs for creating and processing images',
icon: <Image size={20} />,
apis: [
{
name: 'Stability AI',
description: 'AI-powered image generation and editing',
models: ['stable-diffusion-xl', 'stable-diffusion-3'],
pricing: '$0.04 per image generated',
use_cases: ['Image generation', 'Art creation', 'Visual content']
}
]
},
embeddings: {
title: 'Embeddings & Vector APIs',
description: 'APIs for text embeddings and vector operations',
icon: <Database size={20} />,
apis: [
{
name: 'Gemini Embeddings',
description: 'Text embeddings for semantic search and analysis',
models: ['gemini-embedding'],
pricing: '$0.15 per 1M input tokens',
use_cases: ['Semantic search', 'Text similarity', 'Vector databases']
}
]
}
};
const ComprehensiveAPIBreakdown: React.FC<ComprehensiveAPIBreakdownProps> = ({
providerBreakdown,
totalCost
}) => {
// Get active providers from breakdown
const activeProviders = Object.entries(providerBreakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => ({ provider, ...data }));
const getProviderCategory = (providerName: string) => {
const provider = providerName.toLowerCase();
if (['gemini', 'openai', 'anthropic', 'mistral'].includes(provider)) {
return 'llm_models';
}
if (['tavily', 'serper', 'metaphor'].includes(provider)) {
return 'search_apis';
}
if (['firecrawl'].includes(provider)) {
return 'content_processing';
}
if (['stability'].includes(provider)) {
return 'image_generation';
}
return 'llm_models'; // default
};
const getCategoryStats = (categoryKey: string) => {
const categoryProviders = activeProviders.filter(p =>
getProviderCategory(p.provider) === categoryKey
);
return {
count: categoryProviders.length,
totalCost: categoryProviders.reduce((sum, p) => sum + p.cost, 0),
totalCalls: categoryProviders.reduce((sum, p) => sum + p.calls, 0),
totalTokens: categoryProviders.reduce((sum, p) => sum + p.tokens, 0)
};
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', color: '#ffffff' }}>
<BarChart3 size={20} />
Comprehensive API Breakdown
</Typography>
<Tooltip title="Detailed breakdown of all API usage across categories">
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Summary Stats */}
<Box sx={{ mb: 3, p: 2, backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 2 }}>
<Grid container spacing={2}>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{Object.keys(API_CATEGORIES).length}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
API Categories
</Typography>
</Box>
</Grid>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{activeProviders.length}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Active Providers
</Typography>
</Box>
</Grid>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
${totalCost.toFixed(4)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Total Cost
</Typography>
</Box>
</Grid>
<Grid item xs={6} sm={3}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{activeProviders.reduce((sum, p) => sum + p.calls, 0)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Total Calls
</Typography>
</Box>
</Grid>
</Grid>
</Box>
{/* API Categories */}
{Object.entries(API_CATEGORIES).map(([categoryKey, category]) => {
const stats = getCategoryStats(categoryKey);
const hasUsage = stats.count > 0;
return (
<Accordion
key={categoryKey}
sx={{
mb: 1,
backgroundColor: 'rgba(255,255,255,0.05)',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' }
}}
>
<AccordionSummary
expandIcon={<ExpandMore sx={{ color: 'rgba(255,255,255,0.7)' }} />}
sx={{
minHeight: 48,
'&.Mui-expanded': { minHeight: 48 }
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Box sx={{ color: hasUsage ? '#4ade80' : 'rgba(255,255,255,0.5)' }}>
{category.icon}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{category.title}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{category.description}
</Typography>
</Box>
{hasUsage && (
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<Chip
label={`${stats.count} active`}
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80',
border: '1px solid rgba(74, 222, 128, 0.3)'
}}
/>
<Typography variant="caption" sx={{ color: '#4ade80', fontWeight: 'bold' }}>
${stats.totalCost.toFixed(4)}
</Typography>
</Box>
)}
</Box>
</AccordionSummary>
<AccordionDetails sx={{ pt: 0 }}>
<Grid container spacing={2}>
{category.apis.map((api) => {
const providerData = activeProviders.find(p =>
p.provider.toLowerCase() === api.name.toLowerCase()
);
return (
<Grid item xs={12} md={6} key={api.name}>
<Box
sx={{
p: 2,
backgroundColor: providerData ? 'rgba(74, 222, 128, 0.1)' : 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: providerData ? '1px solid rgba(74, 222, 128, 0.2)' : '1px solid rgba(255,255,255,0.1)'
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{api.name}
</Typography>
{providerData && (
<Chip
label="Active"
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80'
}}
/>
)}
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)', display: 'block', mb: 1 }}>
{api.description}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)', display: 'block', mb: 1 }}>
Pricing: {api.pricing}
</Typography>
{providerData && (
<Box sx={{ mt: 2, p: 1, backgroundColor: 'rgba(0,0,0,0.2)', borderRadius: 1 }}>
<Grid container spacing={1}>
<Grid item xs={4}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Cost
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#4ade80', fontWeight: 'bold' }}>
${providerData.cost.toFixed(4)}
</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Calls
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#ffffff', fontWeight: 'bold' }}>
{providerData.calls}
</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Tokens
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: '#ffffff', fontWeight: 'bold' }}>
{providerData.tokens.toLocaleString()}
</Typography>
</Grid>
</Grid>
</Box>
)}
<Box sx={{ mt: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
Use cases: {api.use_cases.join(', ')}
</Typography>
</Box>
</Box>
</Grid>
);
})}
</Grid>
</AccordionDetails>
</Accordion>
);
})}
</CardContent>
</Card>
</motion.div>
);
};
export default ComprehensiveAPIBreakdown;

View File

@@ -0,0 +1,292 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
} from '@mui/material';
import { motion } from 'framer-motion';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from 'recharts';
import {
DollarSign,
TrendingUp,
BarChart3,
PieChart as PieChartIcon
} from 'lucide-react';
// Types
import { ProviderBreakdown } from '../../types/billing';
// Utils
import {
formatCurrency,
formatNumber,
getProviderIcon,
getProviderColor
} from '../../services/billingService';
interface CostBreakdownProps {
providerBreakdown: ProviderBreakdown;
totalCost: number;
}
const CostBreakdown: React.FC<CostBreakdownProps> = ({
providerBreakdown,
totalCost
}) => {
// Transform data for pie chart
const chartData = Object.entries(providerBreakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => ({
name: provider.charAt(0).toUpperCase() + provider.slice(1),
value: data.cost,
calls: data.calls,
tokens: data.tokens,
color: getProviderColor(provider),
icon: getProviderIcon(provider)
}))
.sort((a, b) => b.value - a.value);
// Custom tooltip for pie chart
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<Box
sx={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: 2,
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 1 }}>
{data.icon} {data.name}
</Typography>
<Typography variant="body2">
Cost: {formatCurrency(data.value)}
</Typography>
<Typography variant="body2">
Calls: {formatNumber(data.calls)}
</Typography>
<Typography variant="body2">
Tokens: {formatNumber(data.tokens)}
</Typography>
</Box>
);
}
return null;
};
// Custom label for pie chart
const renderLabel = (entry: any) => {
const percent = ((entry.value / totalCost) * 100).toFixed(1);
return `${entry.name}: ${percent}%`;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', mb: 2 }}>
<PieChartIcon size={20} />
Cost Breakdown by Provider
</Typography>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Pie Chart */}
<Box sx={{ height: 300, mb: 3 }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={renderLabel}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
</Box>
{/* Provider Details */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold' }}>
Provider Details
</Typography>
<Grid container spacing={2}>
{chartData.map((provider, index) => {
const percentage = ((provider.value / totalCost) * 100).toFixed(1);
return (
<Grid item xs={12} sm={6} key={provider.name}>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
position: 'relative'
}}
>
{/* Provider Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span style={{ fontSize: '18px' }}>{provider.icon}</span>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{provider.name}
</Typography>
</Box>
<Chip
label={`${percentage}%`}
size="small"
sx={{
backgroundColor: `${provider.color}20`,
color: provider.color,
fontWeight: 'bold'
}}
/>
</Box>
{/* Metrics */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Cost:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatCurrency(provider.value)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Calls:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(provider.calls)}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Tokens:
</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatNumber(provider.tokens)}
</Typography>
</Box>
{/* Progress bar */}
<Box sx={{ mt: 1 }}>
<Box
sx={{
height: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
borderRadius: 2,
overflow: 'hidden'
}}
>
<motion.div
initial={{ width: 0 }}
animate={{ width: `${percentage}%` }}
transition={{ duration: 1, delay: index * 0.1 }}
style={{
height: '100%',
backgroundColor: provider.color,
borderRadius: 2
}}
/>
</Box>
</Box>
</Box>
</motion.div>
</Grid>
);
})}
</Grid>
</Box>
{/* Summary Stats */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ mb: 1, color: 'rgba(255,255,255,0.8)' }}>
Total Monthly Cost
</Typography>
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{formatCurrency(totalCost)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Across {chartData.length} active providers
</Typography>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default CostBreakdown;

View File

@@ -0,0 +1,392 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Grid,
Card,
CardContent,
Typography,
Alert,
CircularProgress,
useTheme,
useMediaQuery,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Chip,
IconButton,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
DollarSign,
TrendingUp,
AlertTriangle,
Activity,
Zap,
BarChart3,
PieChart,
Clock,
Grid3X3,
List,
Info,
RefreshCw
} from 'lucide-react';
// Services
import { billingService } from '../../services/billingService';
import { monitoringService } from '../../services/monitoringService';
import { onApiEvent } from '../../utils/apiEvents';
// Types
import { DashboardData } from '../../types/billing';
import { SystemHealth } from '../../types/monitoring';
// Components
import CompactBillingDashboard from './CompactBillingDashboard';
import BillingOverview from './BillingOverview';
import SystemHealthIndicator from '../monitoring/SystemHealthIndicator';
import CostBreakdown from './CostBreakdown';
import UsageTrends from './UsageTrends';
import UsageAlerts from './UsageAlerts';
import ComprehensiveAPIBreakdown from './ComprehensiveAPIBreakdown';
interface EnhancedBillingDashboardProps {
userId?: string;
}
type ViewMode = 'compact' | 'detailed';
const EnhancedBillingDashboard: React.FC<EnhancedBillingDashboardProps> = ({ userId }) => {
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [systemHealth, setSystemHealth] = useState<SystemHealth | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>('compact');
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const fetchDashboardData = async () => {
try {
const [billingData, healthData] = await Promise.all([
billingService.getDashboardData(),
monitoringService.getSystemHealth()
]);
setDashboardData(billingData);
setSystemHealth(healthData);
setLastUpdated(new Date());
} catch (error) {
setError(error instanceof Error ? error.message : 'Failed to fetch dashboard data');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
}, [userId]);
// Event-driven refresh: refresh only when non-billing/monitoring APIs complete
useEffect(() => {
const unsubscribe = onApiEvent((detail) => {
if (detail.source && detail.source !== 'other') return;
Promise.all([billingService.getDashboardData(), monitoringService.getSystemHealth()])
.then(([billingData, health]) => {
setDashboardData(billingData);
setSystemHealth(health);
setLastUpdated(new Date());
})
.catch(() => {/* ignore */});
});
return unsubscribe;
}, []);
// Refetch when tab becomes visible again (cheap, avoids polling)
useEffect(() => {
const onVisible = () => {
if (document.visibilityState === 'visible') {
fetchDashboardData();
}
};
document.addEventListener('visibilitychange', onVisible);
return () => document.removeEventListener('visibilitychange', onVisible);
}, []);
const handleViewModeChange = (
event: React.MouseEvent<HTMLElement>,
newViewMode: ViewMode | null,
) => {
if (newViewMode !== null) {
setViewMode(newViewMode);
}
};
if (loading) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
<CircularProgress sx={{ color: 'primary.main' }} />
</Box>
</Container>
);
}
if (error) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
</Container>
);
}
if (!dashboardData) {
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
<Alert severity="warning">
No billing data available. Please check your subscription status.
</Alert>
</Container>
);
}
return (
<Container maxWidth="xl" sx={{ py: 4 }}>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography
variant="h4"
sx={{
fontWeight: 800,
mb: 1.5,
fontSize: '1.1rem',
color: 'rgba(255,255,255,0.95)',
}}
>
Billing & Usage Dashboard
</Typography>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
AI Usage Monitoring
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Track your AI API costs, usage patterns, and system performance in real-time
</Typography>
</Box>
}
arrow
placement="top"
>
<Info size={16} color="rgba(255,255,255,0.6)" style={{ cursor: 'help' }} />
</Tooltip>
</Box>
{/* Active Providers Chips */}
{dashboardData && (
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{Object.entries(dashboardData.current_usage.provider_breakdown)
.filter(([_, data]) => data.cost > 0)
.map(([provider, data]) => (
<Tooltip
key={provider}
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
{provider.toUpperCase()} Usage
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Cost: ${data.cost.toFixed(4)}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Calls: {data.calls.toLocaleString()}
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
Tokens: {data.tokens.toLocaleString()}
</Typography>
</Box>
}
arrow
placement="top"
>
<Chip
label={`${provider}: $${data.cost.toFixed(4)}`}
size="small"
sx={{
backgroundColor: 'rgba(74, 222, 128, 0.2)',
color: '#4ade80',
border: '1px solid rgba(74, 222, 128, 0.3)',
fontSize: '0.7rem',
height: 24,
fontWeight: 500,
'&:hover': {
backgroundColor: 'rgba(74, 222, 128, 0.3)',
transform: 'translateY(-1px)',
boxShadow: '0 4px 12px rgba(74, 222, 128, 0.2)'
},
transition: 'all 0.2s ease'
}}
/>
</Tooltip>
))}
</Box>
)}
</Box>
{/* View Mode Toggle and Refresh */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Tooltip title="Refresh billing data">
<IconButton
size="small"
onClick={fetchDashboardData}
disabled={loading}
sx={{
color: 'rgba(255,255,255,0.7)',
'&:hover': {
color: '#ffffff',
backgroundColor: 'rgba(255,255,255,0.1)'
}
}}
>
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
</IconButton>
</Tooltip>
<Tooltip
title={
<Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5 }}>
View Modes
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
<strong>Compact:</strong> Essential metrics only
</Typography>
<Typography variant="body2" sx={{ opacity: 0.9 }}>
<strong>Detailed:</strong> Full breakdown with charts
</Typography>
</Box>
}
arrow
placement="top"
>
<Info size={16} color="rgba(255,255,255,0.7)" style={{ cursor: 'help' }} />
</Tooltip>
<ToggleButtonGroup
value={viewMode}
exclusive
onChange={handleViewModeChange}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)',
'& .MuiToggleButton-root': {
color: 'rgba(255,255,255,0.7)',
border: 'none',
'&.Mui-selected': {
backgroundColor: 'rgba(255,255,255,0.2)',
color: '#ffffff'
}
}
}}
>
<ToggleButton value="compact">
<Grid3X3 size={16} style={{ marginRight: 8 }} />
Compact
</ToggleButton>
<ToggleButton value="detailed">
<List size={16} style={{ marginRight: 8 }} />
Detailed
</ToggleButton>
</ToggleButtonGroup>
</Box>
</Box>
</Box>
</motion.div>
{/* Dashboard Content */}
<AnimatePresence mode="wait">
{viewMode === 'compact' ? (
<motion.div
key="compact"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
>
<CompactBillingDashboard userId={userId} />
</motion.div>
) : (
<motion.div
key="detailed"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<Grid container spacing={3}>
{/* Top Row */}
<Grid item xs={12} md={4}>
<BillingOverview
usageStats={dashboardData.current_usage}
onRefresh={fetchDashboardData}
/>
</Grid>
<Grid item xs={12} md={4}>
<SystemHealthIndicator
systemHealth={systemHealth}
onRefresh={fetchDashboardData}
/>
</Grid>
<Grid item xs={12} md={4}>
<UsageAlerts
alerts={dashboardData.alerts}
onMarkRead={async (alertId) => {
// TODO: Implement mark as read functionality
console.log('Mark alert as read:', alertId);
}}
/>
</Grid>
{/* Middle Row */}
<Grid item xs={12} md={6}>
<CostBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</Grid>
<Grid item xs={12} md={6}>
<UsageTrends
trends={dashboardData.trends}
projections={dashboardData.projections}
/>
</Grid>
{/* Bottom Row - Comprehensive API Breakdown */}
<Grid item xs={12}>
<ComprehensiveAPIBreakdown
providerBreakdown={dashboardData.current_usage.provider_breakdown}
totalCost={dashboardData.current_usage.total_cost}
/>
</Grid>
</Grid>
</motion.div>
)}
</AnimatePresence>
</Container>
);
};
export default EnhancedBillingDashboard;

View File

@@ -0,0 +1,368 @@
import React, { useState } from 'react';
import {
Card,
CardContent,
Typography,
Box,
List,
ListItem,
ListItemText,
ListItemIcon,
Chip,
IconButton,
Tooltip,
Collapse,
Alert,
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import {
AlertTriangle,
Info,
XCircle,
ChevronDown,
ChevronUp,
Bell,
BellOff,
CheckCircle
} from 'lucide-react';
// Types
import { UsageAlert } from '../../types/billing';
interface UsageAlertsProps {
alerts: UsageAlert[];
onMarkRead: (alertId: number) => Promise<void>;
}
const UsageAlerts: React.FC<UsageAlertsProps> = ({
alerts,
onMarkRead
}) => {
const [expanded, setExpanded] = useState(false);
const [processing, setProcessing] = useState<number | null>(null);
// Separate alerts by read status
const unreadAlerts = alerts.filter(alert => !alert.is_read);
const readAlerts = alerts.filter(alert => alert.is_read);
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'error':
return <XCircle size={16} color="#ef4444" />;
case 'warning':
return <AlertTriangle size={16} color="#f59e0b" />;
case 'info':
return <Info size={16} color="#3b82f6" />;
default:
return <Info size={16} color="#6b7280" />;
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'error':
return '#ef4444';
case 'warning':
return '#f59e0b';
case 'info':
return '#3b82f6';
default:
return '#6b7280';
}
};
const handleMarkAsRead = async (alertId: number) => {
try {
setProcessing(alertId);
await onMarkRead(alertId);
} catch (error) {
console.error('Error marking alert as read:', error);
} finally {
setProcessing(null);
}
};
const formatAlertTime = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffInHours < 1) {
return 'Just now';
} else if (diffInHours < 24) {
return `${diffInHours}h ago`;
} else {
const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays}d ago`;
}
};
const renderAlertItem = (alert: UsageAlert, index: number) => (
<motion.div
key={alert.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
exit={{ opacity: 0, y: -20 }}
>
<ListItem
sx={{
backgroundColor: alert.is_read ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)',
borderRadius: 2,
mb: 1,
border: `1px solid ${getSeverityColor(alert.severity)}20`,
position: 'relative',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.08)',
}
}}
>
<ListItemIcon sx={{ minWidth: 40 }}>
{getSeverityIcon(alert.severity)}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 'bold', flex: 1 }}>
{alert.title}
</Typography>
{!alert.is_read && (
<Chip
label="New"
size="small"
sx={{
backgroundColor: '#ef4444',
color: 'white',
fontSize: '0.7rem',
height: 20
}}
/>
)}
</Box>
}
secondary={
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{alert.message}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="caption" color="text.secondary">
{formatAlertTime(alert.created_at)}
</Typography>
{alert.provider && (
<Chip
label={alert.provider}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
)}
<Chip
label={`${alert.threshold_percentage}% threshold`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
</Box>
</Box>
}
/>
{!alert.is_read && (
<Tooltip title="Mark as read">
<IconButton
size="small"
onClick={() => handleMarkAsRead(alert.id)}
disabled={processing === alert.id}
sx={{
color: getSeverityColor(alert.severity),
'&:hover': {
backgroundColor: `${getSeverityColor(alert.severity)}20`
}
}}
>
{processing === alert.id ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<CheckCircle size={16} />
</motion.div>
) : (
<CheckCircle size={16} />
)}
</IconButton>
</Tooltip>
)}
</ListItem>
</motion.div>
);
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
<Bell size={20} />
Usage Alerts
</Typography>
{unreadAlerts.length > 0 && (
<Chip
label={unreadAlerts.length}
size="small"
sx={{
backgroundColor: '#ef4444',
color: 'white',
fontWeight: 'bold'
}}
/>
)}
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* No alerts state */}
{alerts.length === 0 && (
<Box sx={{ textAlign: 'center', py: 4 }}>
<BellOff size={48} color="#6b7280" />
<Typography variant="body2" sx={{ mt: 2, color: 'rgba(255,255,255,0.8)' }}>
No alerts at this time
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
You'll be notified when usage thresholds are reached
</Typography>
</Box>
)}
{/* Unread alerts */}
{unreadAlerts.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold', color: 'error.main' }}>
Unread Alerts ({unreadAlerts.length})
</Typography>
<List sx={{ p: 0 }}>
<AnimatePresence>
{unreadAlerts.slice(0, 3).map((alert, index) => renderAlertItem(alert, index))}
</AnimatePresence>
</List>
</Box>
)}
{/* Read alerts (collapsible) */}
{readAlerts.length > 0 && (
<Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
cursor: 'pointer',
p: 1,
borderRadius: 1,
'&:hover': { backgroundColor: 'rgba(255,255,255,0.05)' }
}}
onClick={() => setExpanded(!expanded)}
>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: 'text.secondary' }}>
Read Alerts ({readAlerts.length})
</Typography>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</Box>
<Collapse in={expanded}>
<List sx={{ p: 0, mt: 1 }}>
<AnimatePresence>
{readAlerts.map((alert, index) => renderAlertItem(alert, index))}
</AnimatePresence>
</List>
</Collapse>
</Box>
)}
{/* Alert summary */}
{alerts.length > 0 && (
<Box
sx={{
mt: 3,
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Alert Summary
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{['error', 'warning', 'info'].map(severity => {
const count = alerts.filter(alert => alert.severity === severity).length;
if (count === 0) return null;
return (
<Chip
key={severity}
label={`${count} ${severity}`}
size="small"
sx={{
backgroundColor: `${getSeverityColor(severity)}20`,
color: getSeverityColor(severity),
fontSize: '0.7rem',
height: 24
}}
/>
);
})}
</Box>
</Box>
)}
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(239, 68, 68, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(245, 158, 11, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default UsageAlerts;

View File

@@ -0,0 +1,365 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
AreaChart
} from 'recharts';
import {
TrendingUp,
TrendingDown,
BarChart3,
Calendar
} from 'lucide-react';
// Types
import { UsageTrends as UsageTrendsType, CostProjections } from '../../types/billing';
// Utils
import {
formatCurrency,
formatNumber,
formatPercentage
} from '../../services/billingService';
interface UsageTrendsProps {
trends: UsageTrendsType;
projections: CostProjections;
}
const UsageTrends: React.FC<UsageTrendsProps> = ({
trends,
projections
}) => {
// Transform data for charts
const chartData = trends.periods.map((period, index) => ({
period,
calls: trends.total_calls[index] || 0,
cost: trends.total_cost[index] || 0,
tokens: trends.total_tokens[index] || 0,
}));
// Calculate growth rates (handle division by zero)
const costGrowth = chartData.length > 1
? chartData[0].cost > 0
? ((chartData[chartData.length - 1].cost - chartData[0].cost) / chartData[0].cost) * 100
: chartData[chartData.length - 1].cost > 0 ? 100 : 0
: 0;
const callsGrowth = chartData.length > 1
? chartData[0].calls > 0
? ((chartData[chartData.length - 1].calls - chartData[0].calls) / chartData[0].calls) * 100
: chartData[chartData.length - 1].calls > 0 ? 100 : 0
: 0;
// Custom tooltip for charts
const CustomTooltip = ({ active, payload, label }: any) => {
if (active && payload && payload.length) {
return (
<Box
sx={{
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
padding: 2,
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" sx={{ fontWeight: 'bold', mb: 1 }}>
{label}
</Typography>
{payload.map((entry: any, index: number) => (
<Typography key={index} variant="body2" sx={{ color: entry.color }}>
{entry.name}: {entry.name === 'Cost' ? formatCurrency(entry.value) : formatNumber(entry.value)}
</Typography>
))}
</Box>
);
}
return null;
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', mb: 2 }}>
<TrendingUp size={20} />
Usage Trends & Projections
</Typography>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Growth Indicators */}
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={6}>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{costGrowth >= 0 ? (
<TrendingUp size={16} color="#22c55e" />
) : (
<TrendingDown size={16} color="#ef4444" />
)}
<Typography variant="body2" color="text.secondary">
Cost Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: costGrowth >= 0 ? '#22c55e' : '#ef4444'
}}
>
{costGrowth >= 0 ? '+' : ''}{costGrowth.toFixed(1)}%
</Typography>
</Box>
</motion.div>
</Grid>
<Grid item xs={6}>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.1 }}
>
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)',
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
{callsGrowth >= 0 ? (
<TrendingUp size={16} color="#22c55e" />
) : (
<TrendingDown size={16} color="#ef4444" />
)}
<Typography variant="body2" color="text.secondary">
Calls Growth
</Typography>
</Box>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: callsGrowth >= 0 ? '#22c55e' : '#ef4444'
}}
>
{callsGrowth >= 0 ? '+' : ''}{callsGrowth.toFixed(1)}%
</Typography>
</Box>
</motion.div>
</Grid>
</Grid>
{/* Cost Trend Chart */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
Monthly Cost Trend
</Typography>
<Box sx={{ height: 200 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<defs>
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#667eea" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#667eea" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="period"
stroke="rgba(255,255,255,0.9)"
fontSize={12}
tick={{ fill: 'rgba(255,255,255,0.9)' }}
/>
<YAxis
stroke="rgba(255,255,255,0.9)"
fontSize={12}
tick={{ fill: 'rgba(255,255,255,0.9)' }}
tickFormatter={(value) => `$${value.toFixed(0)}`}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="cost"
stroke="#667eea"
strokeWidth={2}
fillOpacity={1}
fill="url(#costGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</Box>
</Box>
{/* API Calls Trend Chart */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', color: '#ffffff' }}>
API Calls Trend
</Typography>
<Box sx={{ height: 150 }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.1)" />
<XAxis
dataKey="period"
stroke="rgba(255,255,255,0.6)"
fontSize={12}
/>
<YAxis
stroke="rgba(255,255,255,0.6)"
fontSize={12}
tickFormatter={(value) => formatNumber(value)}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="calls"
stroke="#764ba2"
strokeWidth={2}
dot={{ fill: '#764ba2', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, stroke: '#764ba2', strokeWidth: 2 }}
/>
</LineChart>
</ResponsiveContainer>
</Box>
</Box>
{/* Projections */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="subtitle2" sx={{ mb: 2, fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: 1 }}>
<Calendar size={16} />
Monthly Projections
</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
Projected Cost
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: 'primary.main' }}>
{formatCurrency(projections.projected_monthly_cost)}
</Typography>
</Box>
</Grid>
<Grid item xs={6}>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
Usage %
</Typography>
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
color: projections.projected_usage_percentage > 80 ? 'error.main' :
projections.projected_usage_percentage > 60 ? 'warning.main' : 'success.main'
}}
>
{formatPercentage(projections.projected_usage_percentage)}
</Typography>
</Box>
</Grid>
</Grid>
<Box sx={{ mt: 2, textAlign: 'center' }}>
<Chip
label={`Limit: ${formatCurrency(projections.cost_limit)}`}
size="small"
sx={{
backgroundColor: 'rgba(255,255,255,0.1)',
color: 'text.secondary',
fontWeight: 'bold'
}}
/>
</Box>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: 'radial-gradient(circle, rgba(118, 75, 162, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: 'radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%)',
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default UsageTrends;

View File

@@ -0,0 +1,329 @@
import React from 'react';
import {
Card,
CardContent,
Typography,
Box,
Chip,
IconButton,
Tooltip,
LinearProgress,
} from '@mui/material';
import { motion } from 'framer-motion';
import {
Activity,
RefreshCw,
AlertTriangle,
CheckCircle,
XCircle,
Clock,
Zap
} from 'lucide-react';
// Types
import { SystemHealth } from '../../types/monitoring';
// Utils
import {
getHealthStatusColor,
getHealthStatusIcon,
formatResponseTime,
formatErrorRate,
getPerformanceStatus
} from '../../services/monitoringService';
interface SystemHealthIndicatorProps {
systemHealth: SystemHealth | null;
onRefresh: () => void;
}
const SystemHealthIndicator: React.FC<SystemHealthIndicatorProps> = ({
systemHealth,
onRefresh
}) => {
if (!systemHealth) {
return (
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography sx={{ color: 'rgba(255,255,255,0.8)' }}>Loading system health...</Typography>
</Card>
);
}
const performanceStatus = getPerformanceStatus(0, systemHealth.error_rate);
const healthColor = getHealthStatusColor(systemHealth.status);
const healthIcon = getHealthStatusIcon(systemHealth.status);
const getStatusChip = () => {
let chipColor: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' = 'default';
if (systemHealth.status === 'healthy') chipColor = 'success';
else if (systemHealth.status === 'warning') chipColor = 'warning';
else if (systemHealth.status === 'critical') chipColor = 'error';
return (
<Chip
icon={<span>{healthIcon}</span>}
label={systemHealth.status.charAt(0).toUpperCase() + systemHealth.status.slice(1)}
color={chipColor}
size="small"
sx={{ fontWeight: 'bold' }}
/>
);
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
whileHover={{ scale: 1.02 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold' }}>
<Activity size={20} />
System Health
</Typography>
<Tooltip title="Refresh data">
<IconButton
size="small"
onClick={onRefresh}
sx={{
color: 'text.secondary',
'&:hover': { color: 'primary.main' }
}}
>
<RefreshCw size={16} />
</IconButton>
</Tooltip>
</Box>
{/* Status Chip */}
<Box sx={{ mb: 3 }}>
{getStatusChip()}
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Main Health Indicator */}
<Box sx={{ mb: 3, textAlign: 'center' }}>
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Box
sx={{
width: 80,
height: 80,
borderRadius: '50%',
background: `linear-gradient(135deg, ${healthColor}20 0%, ${healthColor}10 100%)`,
border: `3px solid ${healthColor}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 16px',
position: 'relative'
}}
>
<Typography variant="h4" sx={{ color: healthColor }}>
{healthIcon}
</Typography>
{/* Pulse animation for critical status */}
{systemHealth.status === 'critical' && (
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1, repeat: Infinity }}
style={{
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: '50%',
border: `2px solid ${healthColor}`,
opacity: 0.3
}}
/>
)}
</Box>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: healthColor }}>
{systemHealth.status.charAt(0).toUpperCase() + systemHealth.status.slice(1)}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
System Status
</Typography>
</motion.div>
</Box>
{/* Metrics */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Recent Requests
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{systemHealth.recent_requests.toLocaleString()}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Recent Errors
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: 'bold',
color: systemHealth.recent_errors > 0 ? '#ff6b6b' : '#ffffff'
}}
>
{systemHealth.recent_errors}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Error Rate
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: 'bold',
color: systemHealth.error_rate > 5 ? '#ff6b6b' : '#ffffff'
}}
>
{formatErrorRate(systemHealth.error_rate)}
</Typography>
</Box>
</Box>
{/* Error Rate Progress */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Error Rate
</Typography>
<Typography variant="body2" fontWeight="bold">
{formatErrorRate(systemHealth.error_rate)}
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={Math.min(systemHealth.error_rate, 100)}
sx={{
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.1)',
'& .MuiLinearProgress-bar': {
backgroundColor: systemHealth.error_rate > 10 ? '#ef4444' :
systemHealth.error_rate > 5 ? '#f59e0b' : '#22c55e',
borderRadius: 4,
}
}}
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{systemHealth.error_rate > 10 ? 'High error rate detected' :
systemHealth.error_rate > 5 ? 'Moderate error rate' : 'Normal error rate'}
</Typography>
</Box>
{/* Performance Indicators */}
<Box
sx={{
p: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
border: '1px solid rgba(255,255,255,0.1)'
}}
>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Performance Status
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<span style={{ color: performanceStatus.color }}>
{performanceStatus.icon}
</span>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{performanceStatus.status.charAt(0).toUpperCase() + performanceStatus.status.slice(1)}
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
Last updated: {new Date(systemHealth.timestamp).toLocaleTimeString()}
</Typography>
</Box>
{/* Quick Actions */}
<Box sx={{ mt: 2, display: 'flex', justifyContent: 'space-around' }}>
<Tooltip title="View detailed logs">
<Box sx={{ textAlign: 'center', cursor: 'pointer' }}>
<Clock size={20} color={healthColor} />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
Logs
</Typography>
</Box>
</Tooltip>
<Tooltip title="Performance metrics">
<Box sx={{ textAlign: 'center', cursor: 'pointer' }}>
<Zap size={20} color={healthColor} />
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
Metrics
</Typography>
</Box>
</Tooltip>
</Box>
</CardContent>
{/* Decorative Elements */}
<Box
sx={{
position: 'absolute',
top: -50,
right: -50,
width: 100,
height: 100,
background: `radial-gradient(circle, ${healthColor}10 0%, transparent 70%)`,
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
<Box
sx={{
position: 'absolute',
bottom: -30,
left: -30,
width: 60,
height: 60,
background: `radial-gradient(circle, ${healthColor}05 0%, transparent 70%)`,
borderRadius: '50%',
pointerEvents: 'none'
}}
/>
</Card>
</motion.div>
);
};
export default SystemHealthIndicator;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, Typography, Chip, Button, CircularProgress } from '@mui/material';
import React, { useState, useEffect } from 'react';
import { Box, Typography, Chip, Button, CircularProgress, Tooltip } from '@mui/material';
import { PlayArrow, Pause, Stop } from '@mui/icons-material';
import { ShimmerHeader } from './styled';
import { DashboardHeaderProps } from './types';
@@ -12,6 +12,47 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
customIcon,
workflowControls
}) => {
// State for enhanced start button behavior
const [isFirstVisit, setIsFirstVisit] = useState(false);
const [showFloatingCTA, setShowFloatingCTA] = useState(false);
const [tooltipMessage, setTooltipMessage] = useState("🎯 Start your daily content workflow here!");
// Check if this is first visit and set up enhanced behavior
useEffect(() => {
const hasVisited = localStorage.getItem('alwrity-has-visited');
if (!hasVisited) {
setIsFirstVisit(true);
localStorage.setItem('alwrity-has-visited', 'true');
// Set up floating CTA after 15 seconds
const timer = setTimeout(() => {
setShowFloatingCTA(true);
// Auto-hide after 30 seconds
setTimeout(() => setShowFloatingCTA(false), 30000);
}, 15000);
return () => clearTimeout(timer);
}
}, []);
// Progressive tooltip messages
useEffect(() => {
if (!isFirstVisit) return;
const messages = [
"🎯 Start your daily content workflow here!",
"💡 This button launches your personalized content plan",
"⚡ Click to begin your digital marketing automation"
];
let messageIndex = 0;
const interval = setInterval(() => {
messageIndex = (messageIndex + 1) % messages.length;
setTooltipMessage(messages[messageIndex]);
}, 10000); // Change message every 10 seconds
return () => clearInterval(interval);
}, [isFirstVisit]);
return (
<ShimmerHeader sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
@@ -59,60 +100,87 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Workflow Control Buttons */}
{!workflowControls.isWorkflowActive ? (
/* Start Button with Badge and Lightning Glow */
/* Enhanced Start Button with Phase 1 Improvements */
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<Button
variant="contained"
size="small"
startIcon={<PlayArrow />}
onClick={workflowControls.onStartWorkflow}
disabled={workflowControls.isLoading}
sx={{
position: 'relative',
overflow: 'hidden',
background: 'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)',
border: '2px solid transparent',
'&:hover': {
background: 'linear-gradient(135deg, #388e3c 0%, #2e7d32 100%)',
},
minWidth: 'auto',
px: 2,
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
animation: 'shimmer 2.5s infinite',
zIndex: 1,
},
'&::after': {
content: '""',
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
background: 'linear-gradient(45deg, #4caf50, #8bc34a, #4caf50, #8bc34a)',
backgroundSize: '400% 400%',
borderRadius: 'inherit',
zIndex: -1,
animation: 'borderGlow 3s ease-in-out infinite',
},
'@keyframes shimmer': {
'0%': { left: '-100%' },
'100%': { left: '100%' },
},
'@keyframes borderGlow': {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
}}
>
Start
</Button>
<Tooltip title={tooltipMessage} arrow placement="bottom">
<Button
variant="contained"
size={isFirstVisit ? "medium" : "small"}
startIcon={<PlayArrow />}
onClick={workflowControls.onStartWorkflow}
disabled={workflowControls.isLoading}
sx={{
position: 'relative',
overflow: 'hidden',
// Phase 1: Orange/Amber color psychology for action
background: 'linear-gradient(135deg, #FF6B35 0%, #E55A2B 100%)',
border: '2px solid transparent',
// Reduced size by 30% for both first visit and returning users
transform: isFirstVisit ? 'scale(0.875)' : 'scale(0.7)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
background: 'linear-gradient(135deg, #E55A2B 0%, #D1491F 100%)',
transform: isFirstVisit ? 'scale(0.95)' : 'scale(0.75)',
},
minWidth: 'auto',
px: isFirstVisit ? 3 : 2,
py: isFirstVisit ? 1.5 : 1,
fontSize: isFirstVisit ? '1rem' : '0.875rem',
fontWeight: 700,
// Phase 1: Enhanced pulsing animation
animation: isFirstVisit ? 'pulse 2s ease-in-out infinite' : 'none',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: '-100%',
width: '100%',
height: '100%',
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
animation: 'shimmer 2.5s infinite',
zIndex: 1,
},
// Phase 1: Stronger outer glow effect
'&::after': {
content: '""',
position: 'absolute',
top: -4,
left: -4,
right: -4,
bottom: -4,
background: 'linear-gradient(45deg, #FF6B35, #FF8C42, #FF6B35, #FF8C42)',
backgroundSize: '400% 400%',
borderRadius: 'inherit',
zIndex: -1,
animation: 'borderGlow 3s ease-in-out infinite',
// Enhanced glow effect
boxShadow: isFirstVisit
? '0 0 20px rgba(255, 107, 53, 0.6), 0 0 40px rgba(255, 107, 53, 0.4), 0 0 60px rgba(255, 107, 53, 0.2)'
: '0 0 15px rgba(255, 107, 53, 0.4), 0 0 30px rgba(255, 107, 53, 0.2)',
},
'@keyframes pulse': {
'0%, 100%': {
transform: isFirstVisit ? 'scale(0.875)' : 'scale(0.7)',
boxShadow: '0 0 20px rgba(255, 107, 53, 0.6)'
},
'50%': {
transform: isFirstVisit ? 'scale(0.95)' : 'scale(0.75)',
boxShadow: '0 0 30px rgba(255, 107, 53, 0.8)'
},
},
'@keyframes shimmer': {
'0%': { left: '-100%' },
'100%': { left: '100%' },
},
'@keyframes borderGlow': {
'0%, 100%': { backgroundPosition: '0% 50%' },
'50%': { backgroundPosition: '100% 50%' },
},
}}
>
{isFirstVisit ? '🚀 Start Journey' : 'Start'}
</Button>
</Tooltip>
<Box
sx={{
position: 'absolute',
@@ -130,6 +198,90 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
>
{`${workflowControls.completedTasks}/${workflowControls.totalTasks}`}
</Box>
{/* Floating CTA for first-time users */}
{showFloatingCTA && isFirstVisit && (
<Box
sx={{
position: 'absolute',
top: '100%',
right: 0,
mt: 2,
p: 2,
backgroundColor: 'rgba(255, 107, 53, 0.95)',
borderRadius: 2,
boxShadow: '0 8px 32px rgba(255, 107, 53, 0.4)',
border: '1px solid rgba(255, 255, 255, 0.2)',
backdropFilter: 'blur(10px)',
zIndex: 1000,
animation: 'fadeInUp 0.5s ease-out',
maxWidth: 280,
'&::before': {
content: '""',
position: 'absolute',
top: -8,
right: 20,
width: 0,
height: 0,
borderLeft: '8px solid transparent',
borderRight: '8px solid transparent',
borderBottom: '8px solid rgba(255, 107, 53, 0.95)',
},
'@keyframes fadeInUp': {
'0%': {
opacity: 0,
transform: 'translateY(20px)',
},
'100%': {
opacity: 1,
transform: 'translateY(0)',
},
},
}}
>
<Typography
variant="body2"
sx={{
color: 'white',
fontWeight: 600,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
🎯 Ready to create amazing content?
</Typography>
<Typography
variant="caption"
sx={{
color: 'rgba(255, 255, 255, 0.9)',
display: 'block',
mb: 1,
}}
>
Click the orange button above to start your personalized content workflow!
</Typography>
<Button
size="small"
variant="outlined"
onClick={() => setShowFloatingCTA(false)}
sx={{
color: 'white',
borderColor: 'rgba(255, 255, 255, 0.5)',
fontSize: '0.75rem',
py: 0.5,
px: 1,
'&:hover': {
borderColor: 'white',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
}}
>
Got it!
</Button>
</Box>
)}
</Box>
) : (
/* In-Progress/Completed Controls with Enhanced Styling */

View File

@@ -25,7 +25,8 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
onSubCategoryChange,
toolCategories,
theme,
onCategoryClick
onCategoryClick,
compact = false
}) => {
// Helper function to get tool count from a category
const getToolCount = (category: any): number => {
@@ -44,6 +45,87 @@ const SearchFilter: React.FC<SearchFilterProps> = ({
'Social Media': 'Platform writers for Facebook, LinkedIn, Twitter, Instagram, YouTube.',
'Dashboards': 'Analytics dashboards: SEO, Social, Website, Strategy, and Calendar.'
};
if (compact) {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Compact Search Input */}
<TextField
fullWidth
size="small"
placeholder="Search tools..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: 'rgba(255,255,255,0.7)' }} />
</InputAdornment>
),
endAdornment: searchQuery && (
<InputAdornment position="end">
<IconButton
size="small"
onClick={onClearSearch}
sx={{ color: 'rgba(255,255,255,0.7)' }}
>
<ClearIcon fontSize="small" />
</IconButton>
</InputAdornment>
),
sx: {
backgroundColor: 'rgba(255,255,255,0.05)',
borderRadius: 2,
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: 'rgba(255,255,255,0.1)',
},
'&:hover fieldset': {
borderColor: 'rgba(255,255,255,0.2)',
},
'&.Mui-focused fieldset': {
borderColor: 'rgba(255,255,255,0.3)',
},
},
'& .MuiInputBase-input': {
color: '#ffffff',
'&::placeholder': {
color: 'rgba(255,255,255,0.5)',
opacity: 1,
},
},
},
}}
/>
{/* Compact Category Filters */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{Object.entries(toolCategories).map(([categoryId, category]) => (
<CategoryChip
key={categoryId}
label={`${categoryId} (${getToolCount(category)})`}
onClick={() => onCategoryClick?.(categoryId, category)}
sx={{
fontSize: '0.75rem',
height: 24,
backgroundColor: selectedCategory === categoryId
? 'rgba(255,255,255,0.2)'
: 'rgba(255,255,255,0.05)',
color: '#ffffff',
border: '1px solid rgba(255,255,255,0.1)',
'& .MuiChip-label': {
px: 1,
},
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.15)',
}
}}
/>
))}
</Box>
</Box>
);
}
return (
<SearchContainer>
{/* Single Row Layout: Search Input + Category Filters */}

View File

@@ -303,7 +303,7 @@ const ContentDistributionPie: React.FC<ContentDistributionPieProps> = ({
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${((percent || 0) * 100).toFixed(0)}%`}
label={({ name, value }) => `${name} ${value}`}
outerRadius={80}
fill="#8884d8"
dataKey="value"

View File

@@ -74,6 +74,7 @@ export interface SearchFilterProps {
toolCategories: ToolCategories;
theme: any;
onCategoryClick?: (category: string | null, categoryData?: any) => void;
compact?: boolean;
}
export interface DashboardHeaderProps {