ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
167
frontend/src/stores/README.md
Normal file
167
frontend/src/stores/README.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Zustand Store Implementation
|
||||
|
||||
This directory contains Zustand stores for managing state across the Alwrity dashboard components.
|
||||
|
||||
## Overview
|
||||
|
||||
Zustand has been implemented to replace the previous state management approach using custom hooks and local state. This provides:
|
||||
|
||||
- **Centralized state management** across components
|
||||
- **Automatic persistence** with Zustand's persist middleware
|
||||
- **Better performance** with selective re-renders
|
||||
- **Simpler state updates** with immer-like syntax
|
||||
- **Better debugging** with Redux DevTools support
|
||||
|
||||
## Stores
|
||||
|
||||
### 1. `dashboardStore.ts` - Main Dashboard Store
|
||||
Manages state for the main dashboard including:
|
||||
- Search and filter state
|
||||
- Favorites management
|
||||
- Snackbar notifications
|
||||
- Loading and error states
|
||||
|
||||
**Key Features:**
|
||||
- Automatic persistence of favorites and filter preferences
|
||||
- Snackbar management with automatic hiding
|
||||
- Optimized re-renders with selective state subscriptions
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { useDashboardStore } from '../stores/dashboardStore';
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
searchQuery,
|
||||
favorites,
|
||||
toggleFavorite,
|
||||
setSearchQuery,
|
||||
showSnackbar,
|
||||
} = useDashboardStore();
|
||||
```
|
||||
|
||||
### 2. `seoDashboardStore.ts` - SEO Dashboard Store
|
||||
Manages state for the SEO dashboard including:
|
||||
- Dashboard data fetching and caching
|
||||
- Loading and error states
|
||||
- Data refresh functionality
|
||||
|
||||
**Key Features:**
|
||||
- Automatic data fetching on component mount
|
||||
- Error handling with retry functionality
|
||||
- Data caching with last updated timestamp
|
||||
- DevTools integration for debugging
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { useSEODashboardStore } from '../stores/seoDashboardStore';
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
fetchDashboardData,
|
||||
refreshData,
|
||||
} = useSEODashboardStore();
|
||||
```
|
||||
|
||||
### 3. `sharedDashboardStore.ts` - Shared Dashboard Store
|
||||
Manages common functionality across all dashboards:
|
||||
- Sidebar state
|
||||
- Theme management
|
||||
- Global notifications
|
||||
|
||||
**Key Features:**
|
||||
- Theme switching with system preference detection
|
||||
- Notification management with auto-cleanup
|
||||
- Sidebar state management
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import { useSharedDashboardStore } from '../stores/sharedDashboardStore';
|
||||
|
||||
const {
|
||||
isSidebarOpen,
|
||||
currentTheme,
|
||||
notifications,
|
||||
toggleSidebar,
|
||||
setTheme,
|
||||
addNotification,
|
||||
} = useSharedDashboardStore();
|
||||
```
|
||||
|
||||
## Benefits Over Previous Implementation
|
||||
|
||||
### Before (Custom Hooks + Local State)
|
||||
```typescript
|
||||
// MainDashboard - Custom hook with manual localStorage
|
||||
const useDashboardState = () => {
|
||||
const [state, setState] = useState<DashboardState>({...});
|
||||
// Manual localStorage handling
|
||||
// Complex state updates
|
||||
// No cross-component communication
|
||||
};
|
||||
|
||||
// SEODashboard - Local state
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [data, setData] = useState<SEODashboardData | null>(null);
|
||||
```
|
||||
|
||||
### After (Zustand Stores)
|
||||
```typescript
|
||||
// Centralized, persistent, and optimized
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
fetchDashboardData,
|
||||
} = useSEODashboardStore();
|
||||
|
||||
// Automatic persistence
|
||||
const {
|
||||
favorites,
|
||||
searchQuery,
|
||||
toggleFavorite,
|
||||
} = useDashboardStore();
|
||||
```
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
1. **Selective Re-renders**: Components only re-render when their specific state changes
|
||||
2. **Automatic Persistence**: No manual localStorage management needed
|
||||
3. **Optimized Updates**: Zustand's internal optimizations reduce unnecessary renders
|
||||
4. **DevTools Integration**: Better debugging and state inspection
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- The old `useDashboardState` hook can be removed after confirming the new implementation works correctly
|
||||
- All localStorage operations are now handled automatically by Zustand's persist middleware
|
||||
- Error handling is more robust with centralized error states
|
||||
- Snackbar management is simplified with automatic cleanup
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Real-time Updates**: Can easily add WebSocket integration for live data updates
|
||||
2. **Offline Support**: Zustand's persistence can be extended for offline functionality
|
||||
3. **State Synchronization**: Multiple tabs can share state through storage events
|
||||
4. **Advanced Caching**: Can implement more sophisticated caching strategies
|
||||
|
||||
## Testing
|
||||
|
||||
The stores can be tested independently:
|
||||
```typescript
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useDashboardStore } from './dashboardStore';
|
||||
|
||||
test('should toggle favorite', () => {
|
||||
const { result } = renderHook(() => useDashboardStore());
|
||||
|
||||
act(() => {
|
||||
result.current.toggleFavorite('test-tool');
|
||||
});
|
||||
|
||||
expect(result.current.favorites).toContain('test-tool');
|
||||
});
|
||||
```
|
||||
680
frontend/src/stores/contentPlanningStore.ts
Normal file
680
frontend/src/stores/contentPlanningStore.ts
Normal file
@@ -0,0 +1,680 @@
|
||||
import { create } from 'zustand';
|
||||
import { contentPlanningApi } from '../services/contentPlanningApi';
|
||||
|
||||
// Types
|
||||
export interface ContentStrategy {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
industry: string;
|
||||
target_audience: string;
|
||||
content_pillars: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
user_id?: number;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
scheduled_date?: string;
|
||||
platform: string;
|
||||
content_type: string;
|
||||
status: 'draft' | 'scheduled' | 'published';
|
||||
strategy_id?: string;
|
||||
user_id?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ContentGapAnalysis {
|
||||
id: string;
|
||||
website_url: string;
|
||||
competitors: string[];
|
||||
keywords: string[];
|
||||
gaps: string[];
|
||||
recommendations: AIRecommendation[];
|
||||
created_at: string;
|
||||
user_id?: number;
|
||||
}
|
||||
|
||||
export interface AIRecommendation {
|
||||
id: string;
|
||||
type: 'strategy' | 'topic' | 'timing' | 'platform' | 'optimization';
|
||||
title: string;
|
||||
description: string;
|
||||
confidence: number;
|
||||
reasoning: string;
|
||||
action_items: string[];
|
||||
status: 'pending' | 'accepted' | 'rejected' | 'modified';
|
||||
}
|
||||
|
||||
export interface AIInsight {
|
||||
id: string;
|
||||
type: 'performance' | 'opportunity' | 'warning' | 'trend';
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PerformanceMetrics {
|
||||
engagement: number;
|
||||
reach: number;
|
||||
conversion: number;
|
||||
roi: number;
|
||||
time_range: string;
|
||||
}
|
||||
|
||||
// New Calendar Generation Types
|
||||
export interface GeneratedCalendar {
|
||||
user_id: number;
|
||||
strategy_id?: number;
|
||||
calendar_type: string;
|
||||
industry: string;
|
||||
business_size: string;
|
||||
generated_at: string;
|
||||
content_pillars: string[];
|
||||
platform_strategies: any;
|
||||
content_mix: Record<string, number>;
|
||||
daily_schedule: any[];
|
||||
weekly_themes: any[];
|
||||
content_recommendations: any[];
|
||||
optimal_timing: any;
|
||||
performance_predictions: any;
|
||||
trending_topics: any[];
|
||||
repurposing_opportunities: any[];
|
||||
ai_insights: any[];
|
||||
competitor_analysis: any;
|
||||
gap_analysis_insights: any;
|
||||
strategy_insights: any;
|
||||
onboarding_insights: any;
|
||||
processing_time: number;
|
||||
ai_confidence: number;
|
||||
}
|
||||
|
||||
export interface ContentOptimization {
|
||||
user_id: number;
|
||||
event_id?: number;
|
||||
original_content: any;
|
||||
optimized_content: any;
|
||||
platform_adaptations: string[];
|
||||
visual_recommendations: string[];
|
||||
hashtag_suggestions: string[];
|
||||
keyword_optimization: any;
|
||||
tone_adjustments: any;
|
||||
length_optimization: any;
|
||||
performance_prediction: any;
|
||||
optimization_score: number;
|
||||
recommendations?: any[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PerformancePrediction {
|
||||
user_id: number;
|
||||
strategy_id?: number;
|
||||
content_type: string;
|
||||
platform: string;
|
||||
predicted_engagement_rate: number;
|
||||
predicted_reach: number;
|
||||
predicted_conversions: number;
|
||||
predicted_roi: number;
|
||||
confidence_score: number;
|
||||
recommendations: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ContentRepurposing {
|
||||
user_id: number;
|
||||
strategy_id?: number;
|
||||
original_content: any;
|
||||
platform_adaptations: any[];
|
||||
transformations: any[];
|
||||
implementation_tips: string[];
|
||||
gap_addresses: string[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface TrendingTopics {
|
||||
user_id: number;
|
||||
industry: string;
|
||||
trending_topics: any[];
|
||||
gap_relevance_scores: Record<string, number>;
|
||||
audience_alignment_scores: Record<string, number>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Store interface
|
||||
interface ContentPlanningStore {
|
||||
// State
|
||||
strategies: ContentStrategy[];
|
||||
currentStrategy: ContentStrategy | null;
|
||||
calendarEvents: CalendarEvent[];
|
||||
gapAnalyses: ContentGapAnalysis[];
|
||||
aiRecommendations: AIRecommendation[];
|
||||
aiInsights: AIInsight[];
|
||||
performanceMetrics: PerformanceMetrics | null;
|
||||
|
||||
// New Calendar Generation State
|
||||
generatedCalendar: GeneratedCalendar | null;
|
||||
contentOptimization: ContentOptimization | null;
|
||||
performancePrediction: PerformancePrediction | null;
|
||||
contentRepurposing: ContentRepurposing | null;
|
||||
trendingTopics: TrendingTopics | null;
|
||||
calendarGenerationLoading: boolean;
|
||||
calendarGenerationError: string | null;
|
||||
|
||||
// UI state
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
activeTab: 'strategy' | 'calendar' | 'analytics' | 'gaps';
|
||||
dataLoading: boolean;
|
||||
|
||||
// Actions
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setActiveTab: (tab: 'strategy' | 'calendar' | 'analytics' | 'gaps') => void;
|
||||
|
||||
// Strategy actions
|
||||
createStrategy: (strategy: Omit<ContentStrategy, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
||||
updateStrategy: (id: string, updates: Partial<ContentStrategy>) => Promise<void>;
|
||||
deleteStrategy: (id: string) => Promise<void>;
|
||||
setCurrentStrategy: (strategy: ContentStrategy | null) => void;
|
||||
|
||||
// Calendar actions
|
||||
createEvent: (event: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
||||
updateEvent: (id: string, updates: Partial<CalendarEvent>) => Promise<void>;
|
||||
deleteEvent: (id: string) => Promise<void>;
|
||||
|
||||
// Gap analysis actions
|
||||
createGapAnalysis: (analysis: Omit<ContentGapAnalysis, 'id' | 'created_at'>) => Promise<void>;
|
||||
updateGapAnalysis: (id: string, updates: Partial<ContentGapAnalysis>) => Promise<void>;
|
||||
analyzeContentGaps: (params: { website_url: string; competitors: string[]; keywords: string[] }) => Promise<void>;
|
||||
|
||||
// AI actions
|
||||
addAIRecommendation: (recommendation: AIRecommendation) => void;
|
||||
updateAIRecommendation: (id: string, status: AIRecommendation['status']) => void;
|
||||
addAIInsight: (insight: AIInsight) => void;
|
||||
|
||||
// Analytics actions
|
||||
setPerformanceMetrics: (metrics: PerformanceMetrics) => void;
|
||||
|
||||
// Load data
|
||||
loadStrategies: () => Promise<void>;
|
||||
loadCalendarEvents: () => Promise<void>;
|
||||
loadGapAnalyses: () => Promise<void>;
|
||||
loadAIInsights: () => Promise<void>;
|
||||
loadAIRecommendations: () => Promise<void>;
|
||||
|
||||
// Update data (for orchestrator)
|
||||
updateStrategies: (strategies: ContentStrategy[]) => void;
|
||||
updateCalendarEvents: (events: CalendarEvent[]) => void;
|
||||
updateGapAnalyses: (analyses: ContentGapAnalysis[]) => void;
|
||||
updateAIInsights: (data: { insights: AIInsight[]; recommendations: AIRecommendation[] }) => void;
|
||||
|
||||
// Health checks
|
||||
checkHealth: () => Promise<boolean>;
|
||||
checkDatabaseHealth: () => Promise<boolean>;
|
||||
|
||||
// New Calendar Generation Actions
|
||||
generateCalendar: (request: {
|
||||
user_id: number;
|
||||
strategy_id?: number;
|
||||
calendar_type: string;
|
||||
industry?: string;
|
||||
business_size: string;
|
||||
force_refresh?: boolean;
|
||||
}) => Promise<void>;
|
||||
|
||||
optimizeContent: (request: {
|
||||
user_id: number;
|
||||
event_id?: number;
|
||||
title: string;
|
||||
description: string;
|
||||
content_type: string;
|
||||
target_platform: string;
|
||||
original_content?: any;
|
||||
}) => Promise<void>;
|
||||
|
||||
predictPerformance: (request: {
|
||||
user_id: number;
|
||||
strategy_id?: number;
|
||||
content_type: string;
|
||||
platform: string;
|
||||
content_data: any;
|
||||
}) => Promise<void>;
|
||||
|
||||
repurposeContent: (request: {
|
||||
user_id: number;
|
||||
strategy_id?: number;
|
||||
original_content: any;
|
||||
target_platforms: string[];
|
||||
}) => Promise<void>;
|
||||
|
||||
getTrendingTopics: (request: {
|
||||
user_id: number;
|
||||
industry: string;
|
||||
limit?: number;
|
||||
}) => Promise<void>;
|
||||
|
||||
setCalendarGenerationLoading: (loading: boolean) => void;
|
||||
setCalendarGenerationError: (error: string | null) => void;
|
||||
clearCalendarGenerationData: () => void;
|
||||
}
|
||||
|
||||
// Store implementation
|
||||
export const useContentPlanningStore = create<ContentPlanningStore>((set, get) => ({
|
||||
// Initial state
|
||||
strategies: [],
|
||||
currentStrategy: null,
|
||||
calendarEvents: [],
|
||||
gapAnalyses: [],
|
||||
aiRecommendations: [],
|
||||
aiInsights: [],
|
||||
performanceMetrics: null,
|
||||
|
||||
// New Calendar Generation State
|
||||
generatedCalendar: null,
|
||||
contentOptimization: null,
|
||||
performancePrediction: null,
|
||||
contentRepurposing: null,
|
||||
trendingTopics: null,
|
||||
calendarGenerationLoading: false,
|
||||
calendarGenerationError: null,
|
||||
|
||||
loading: false,
|
||||
error: null,
|
||||
activeTab: 'strategy',
|
||||
dataLoading: false,
|
||||
|
||||
// UI actions
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setActiveTab: (activeTab) => set({ activeTab }),
|
||||
|
||||
// Strategy actions
|
||||
createStrategy: async (strategy) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const newStrategy = await contentPlanningApi.createStrategySafe({
|
||||
name: strategy.name,
|
||||
description: strategy.description,
|
||||
industry: strategy.industry,
|
||||
target_audience: strategy.target_audience,
|
||||
content_pillars: strategy.content_pillars,
|
||||
user_id: strategy.user_id
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
strategies: [...state.strategies, newStrategy],
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to create strategy', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateStrategy: async (id, updates) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const updatedStrategy = await contentPlanningApi.updateStrategy(id, updates);
|
||||
set((state) => ({
|
||||
strategies: state.strategies.map((strategy) =>
|
||||
strategy.id === id ? updatedStrategy : strategy
|
||||
),
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to update strategy', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
deleteStrategy: async (id) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
await contentPlanningApi.deleteStrategy(id);
|
||||
set((state) => ({
|
||||
strategies: state.strategies.filter((strategy) => strategy.id !== id),
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to delete strategy', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentStrategy: (strategy) => set({ currentStrategy: strategy }),
|
||||
|
||||
// Calendar actions
|
||||
createEvent: async (event) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const newEvent = await contentPlanningApi.createEventSafe({
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
date: event.date,
|
||||
platform: event.platform,
|
||||
content_type: event.content_type,
|
||||
status: event.status,
|
||||
strategy_id: event.strategy_id,
|
||||
user_id: event.user_id
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
calendarEvents: [...state.calendarEvents, newEvent],
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to create event', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateEvent: async (id, updates) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const updatedEvent = await contentPlanningApi.updateEvent(id, updates);
|
||||
set((state) => ({
|
||||
calendarEvents: state.calendarEvents.map((event) =>
|
||||
event.id === id ? updatedEvent : event
|
||||
),
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to update event', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
deleteEvent: async (id) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
await contentPlanningApi.deleteEvent(id);
|
||||
set((state) => ({
|
||||
calendarEvents: state.calendarEvents.filter((event) => event.id !== id),
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to delete event', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Gap analysis actions
|
||||
createGapAnalysis: async (analysis) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const newAnalysis = await contentPlanningApi.createGapAnalysisSafe({
|
||||
website_url: analysis.website_url,
|
||||
competitors: analysis.competitors,
|
||||
keywords: analysis.keywords,
|
||||
user_id: analysis.user_id
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
gapAnalyses: [...state.gapAnalyses, newAnalysis],
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to create gap analysis', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateGapAnalysis: async (id, updates) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const updatedAnalysis = await contentPlanningApi.updateGapAnalysis(id, updates);
|
||||
set((state) => ({
|
||||
gapAnalyses: state.gapAnalyses.map((analysis) =>
|
||||
analysis.id === id ? updatedAnalysis : analysis
|
||||
),
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to update gap analysis', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
analyzeContentGaps: async (params) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const analysisResult = await contentPlanningApi.analyzeContentGapsSafe(params);
|
||||
|
||||
// Add the analysis result to the store
|
||||
set((state) => ({
|
||||
gapAnalyses: [...state.gapAnalyses, analysisResult],
|
||||
loading: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to analyze content gaps', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// AI actions
|
||||
addAIRecommendation: (recommendation) => {
|
||||
set((state) => ({
|
||||
aiRecommendations: [...state.aiRecommendations, recommendation],
|
||||
}));
|
||||
},
|
||||
|
||||
updateAIRecommendation: (id, status) => {
|
||||
set((state) => ({
|
||||
aiRecommendations: state.aiRecommendations.map((rec) =>
|
||||
rec.id === id ? { ...rec, status } : rec
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
addAIInsight: (insight) => {
|
||||
set((state) => ({
|
||||
aiInsights: [...state.aiInsights, insight],
|
||||
}));
|
||||
},
|
||||
|
||||
// Analytics actions
|
||||
setPerformanceMetrics: (metrics) => set({ performanceMetrics: metrics }),
|
||||
|
||||
// Load data actions
|
||||
loadStrategies: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const strategies = await contentPlanningApi.getStrategiesSafe();
|
||||
set({ strategies, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load strategies', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadCalendarEvents: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const events = await contentPlanningApi.getEventsSafe();
|
||||
set({ calendarEvents: events, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load calendar events', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadGapAnalyses: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const analyses = await contentPlanningApi.getGapAnalyses();
|
||||
set({ gapAnalyses: analyses, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load gap analyses', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadAIInsights: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await contentPlanningApi.getAIAnalyticsSafe();
|
||||
|
||||
// Validate response structure
|
||||
if (!response || typeof response !== 'object') {
|
||||
console.warn('Invalid AI analytics response:', response);
|
||||
set({ aiInsights: [], loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the response structure - it returns an object with insights array
|
||||
const insights = Array.isArray(response.insights) ? response.insights : [];
|
||||
|
||||
// If no insights from backend, create some default insights from recommendations
|
||||
let transformedInsights = insights;
|
||||
if (insights.length === 0 && response.recommendations && Array.isArray(response.recommendations)) {
|
||||
transformedInsights = response.recommendations.slice(0, 3).map((rec: any, index: number) => ({
|
||||
id: `insight_${Date.now()}_${index}`,
|
||||
type: 'opportunity',
|
||||
title: rec.title || 'AI Insight',
|
||||
description: rec.description || 'AI-generated insight',
|
||||
priority: rec.priority === 'High' ? 'high' : rec.priority === 'Medium' ? 'medium' : 'low',
|
||||
created_at: new Date().toISOString()
|
||||
}));
|
||||
} else {
|
||||
// Transform insights data to the expected format
|
||||
transformedInsights = insights.map((insight: any) => ({
|
||||
id: insight.id || `insight_${Date.now()}`,
|
||||
type: insight.type || 'performance',
|
||||
title: insight.title || 'AI Insight',
|
||||
description: insight.description || 'AI-generated insight',
|
||||
priority: insight.priority || 'medium',
|
||||
created_at: insight.created_at || new Date().toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
set({ aiInsights: transformedInsights, loading: false });
|
||||
} catch (error: any) {
|
||||
console.error('Error loading AI insights:', error);
|
||||
set({ error: error.message || 'Failed to load AI insights', loading: false, aiInsights: [] });
|
||||
}
|
||||
},
|
||||
|
||||
loadAIRecommendations: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const response = await contentPlanningApi.getAIAnalyticsSafe();
|
||||
|
||||
// Validate response structure
|
||||
if (!response || typeof response !== 'object') {
|
||||
console.warn('Invalid AI analytics response:', response);
|
||||
set({ aiRecommendations: [], loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the response structure - it returns an object with recommendations array
|
||||
const recommendations = Array.isArray(response.recommendations) ? response.recommendations : [];
|
||||
|
||||
// Transform recommendations data to the expected format
|
||||
const transformedRecommendations = recommendations.map((rec: any, index: number) => ({
|
||||
id: rec.id || `rec_${Date.now()}_${index}`,
|
||||
type: rec.type?.toLowerCase() || 'strategy',
|
||||
title: rec.title || 'AI Recommendation',
|
||||
description: rec.description || 'AI-generated recommendation',
|
||||
confidence: rec.ai_confidence || rec.confidence || 0.8,
|
||||
reasoning: rec.reasoning || rec.description || 'Generated by AI analysis',
|
||||
action_items: Array.isArray(rec.content_suggestions) ? rec.content_suggestions : [],
|
||||
status: rec.status || 'pending'
|
||||
}));
|
||||
|
||||
set({ aiRecommendations: transformedRecommendations, loading: false });
|
||||
} catch (error: any) {
|
||||
console.error('Error loading AI recommendations:', error);
|
||||
set({ error: error.message || 'Failed to load AI recommendations', loading: false, aiRecommendations: [] });
|
||||
}
|
||||
},
|
||||
|
||||
// Update data (for orchestrator)
|
||||
updateStrategies: (strategies: ContentStrategy[]) => {
|
||||
set({ strategies });
|
||||
},
|
||||
|
||||
updateCalendarEvents: (events: CalendarEvent[]) => {
|
||||
set({ calendarEvents: events });
|
||||
},
|
||||
|
||||
updateGapAnalyses: (analyses: ContentGapAnalysis[]) => {
|
||||
set({ gapAnalyses: analyses });
|
||||
},
|
||||
|
||||
updateAIInsights: (data: { insights: AIInsight[]; recommendations: AIRecommendation[] }) => {
|
||||
set({
|
||||
aiInsights: data.insights,
|
||||
aiRecommendations: data.recommendations
|
||||
});
|
||||
},
|
||||
|
||||
// Health checks
|
||||
checkHealth: async () => {
|
||||
try {
|
||||
const health = await contentPlanningApi.checkHealth();
|
||||
return health.status === 'healthy';
|
||||
} catch (error) {
|
||||
console.error('Health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
checkDatabaseHealth: async () => {
|
||||
try {
|
||||
const dbHealth = await contentPlanningApi.checkDatabaseHealth();
|
||||
return dbHealth.status === 'healthy';
|
||||
} catch (error) {
|
||||
console.error('Database health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// New Calendar Generation Actions
|
||||
generateCalendar: async (request) => {
|
||||
set({ calendarGenerationLoading: true, calendarGenerationError: null });
|
||||
try {
|
||||
const generatedCalendar = await contentPlanningApi.generateCalendar(request);
|
||||
set({ generatedCalendar, calendarGenerationLoading: false });
|
||||
} catch (error: any) {
|
||||
set({ calendarGenerationError: error.message || 'Failed to generate calendar', calendarGenerationLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
optimizeContent: async (request) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const optimizedContent = await contentPlanningApi.optimizeContent(request);
|
||||
set({ contentOptimization: optimizedContent, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to optimize content', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
predictPerformance: async (request) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const performancePrediction = await contentPlanningApi.predictPerformance(request);
|
||||
set({ performancePrediction, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to predict performance', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
repurposeContent: async (request) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const contentRepurposing = await contentPlanningApi.repurposeContent(request);
|
||||
set({ contentRepurposing, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to repurpose content', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
getTrendingTopics: async (request) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const trendingTopics = await contentPlanningApi.getTrendingTopics(request);
|
||||
set({ trendingTopics, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to get trending topics', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
setCalendarGenerationLoading: (loading) => set({ calendarGenerationLoading: loading }),
|
||||
setCalendarGenerationError: (error) => set({ calendarGenerationError: error }),
|
||||
clearCalendarGenerationData: () => set({ generatedCalendar: null, contentOptimization: null, performancePrediction: null, contentRepurposing: null, trendingTopics: null }),
|
||||
}));
|
||||
110
frontend/src/stores/dashboardStore.ts
Normal file
110
frontend/src/stores/dashboardStore.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { DashboardState, SnackbarState } from '../components/shared/types';
|
||||
|
||||
export interface DashboardStore extends DashboardState {
|
||||
// Actions
|
||||
toggleFavorite: (toolName: string) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
setSelectedCategory: (category: string | null) => void;
|
||||
setSelectedSubCategory: (subCategory: string | null) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
showSnackbar: (message: string, severity?: SnackbarState['severity']) => void;
|
||||
hideSnackbar: () => void;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
export const useDashboardStore = create<DashboardStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
loading: false,
|
||||
error: null,
|
||||
searchQuery: '',
|
||||
selectedCategory: null,
|
||||
selectedSubCategory: null,
|
||||
favorites: [],
|
||||
snackbar: {
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'info',
|
||||
},
|
||||
|
||||
// Actions
|
||||
toggleFavorite: (toolName: string) => {
|
||||
const { favorites } = get();
|
||||
const newFavorites = favorites.includes(toolName)
|
||||
? favorites.filter(f => f !== toolName)
|
||||
: [...favorites, toolName];
|
||||
|
||||
set({ favorites: newFavorites });
|
||||
|
||||
// Show snackbar
|
||||
const message = favorites.includes(toolName)
|
||||
? 'Removed from favorites'
|
||||
: 'Added to favorites';
|
||||
get().showSnackbar(message, 'success');
|
||||
},
|
||||
|
||||
setSearchQuery: (query: string) => {
|
||||
set({ searchQuery: query });
|
||||
},
|
||||
|
||||
setSelectedCategory: (category: string | null) => {
|
||||
set({
|
||||
selectedCategory: category,
|
||||
selectedSubCategory: null, // Reset sub-category when changing main category
|
||||
});
|
||||
},
|
||||
|
||||
setSelectedSubCategory: (subCategory: string | null) => {
|
||||
set({ selectedSubCategory: subCategory });
|
||||
},
|
||||
|
||||
setError: (error: string | null) => {
|
||||
set({ error });
|
||||
},
|
||||
|
||||
setLoading: (loading: boolean) => {
|
||||
set({ loading });
|
||||
},
|
||||
|
||||
showSnackbar: (message: string, severity: SnackbarState['severity'] = 'info') => {
|
||||
set({
|
||||
snackbar: {
|
||||
open: true,
|
||||
message,
|
||||
severity,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
hideSnackbar: () => {
|
||||
set({
|
||||
snackbar: {
|
||||
...get().snackbar,
|
||||
open: false,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
clearFilters: () => {
|
||||
set({
|
||||
searchQuery: '',
|
||||
selectedCategory: null,
|
||||
selectedSubCategory: null,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'alwrity-dashboard-storage',
|
||||
partialize: (state) => ({
|
||||
favorites: state.favorites,
|
||||
searchQuery: state.searchQuery,
|
||||
selectedCategory: state.selectedCategory,
|
||||
selectedSubCategory: state.selectedSubCategory,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
944
frontend/src/stores/enhancedStrategyStore.ts
Normal file
944
frontend/src/stores/enhancedStrategyStore.ts
Normal file
@@ -0,0 +1,944 @@
|
||||
import { create } from 'zustand';
|
||||
import { contentPlanningApi } from '../services/contentPlanningApi';
|
||||
|
||||
// Enhanced Strategy Types
|
||||
export interface EnhancedStrategy {
|
||||
id: string;
|
||||
user_id: number;
|
||||
name: string;
|
||||
industry: string;
|
||||
|
||||
// Business Context (8 inputs)
|
||||
business_objectives?: any;
|
||||
target_metrics?: any;
|
||||
content_budget?: number;
|
||||
team_size?: number;
|
||||
implementation_timeline?: string;
|
||||
market_share?: string;
|
||||
competitive_position?: string;
|
||||
performance_metrics?: any;
|
||||
|
||||
// Audience Intelligence (6 inputs)
|
||||
content_preferences?: any;
|
||||
consumption_patterns?: any;
|
||||
audience_pain_points?: any;
|
||||
buying_journey?: any;
|
||||
seasonal_trends?: any;
|
||||
engagement_metrics?: any;
|
||||
|
||||
// Competitive Intelligence (5 inputs)
|
||||
top_competitors?: any;
|
||||
competitor_content_strategies?: any;
|
||||
market_gaps?: any;
|
||||
industry_trends?: any;
|
||||
emerging_trends?: any;
|
||||
|
||||
// Content Strategy (7 inputs)
|
||||
preferred_formats?: any;
|
||||
content_mix?: any;
|
||||
content_frequency?: string;
|
||||
optimal_timing?: any;
|
||||
quality_metrics?: any;
|
||||
editorial_guidelines?: any;
|
||||
brand_voice?: any;
|
||||
|
||||
// Performance & Analytics (4 inputs)
|
||||
traffic_sources?: any;
|
||||
conversion_rates?: any;
|
||||
content_roi_targets?: any;
|
||||
ab_testing_capabilities?: boolean;
|
||||
|
||||
// Enhanced AI Analysis
|
||||
comprehensive_ai_analysis?: any;
|
||||
onboarding_data_used?: any;
|
||||
strategic_scores?: any;
|
||||
market_positioning?: any;
|
||||
competitive_advantages?: any;
|
||||
strategic_risks?: any;
|
||||
opportunity_analysis?: any;
|
||||
|
||||
// Metadata
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completion_percentage: number;
|
||||
data_source_transparency?: any;
|
||||
}
|
||||
|
||||
export interface EnhancedAIAnalysis {
|
||||
id: string;
|
||||
user_id: number;
|
||||
strategy_id: string;
|
||||
analysis_type: string;
|
||||
comprehensive_insights?: any;
|
||||
audience_intelligence?: any;
|
||||
competitive_intelligence?: any;
|
||||
performance_optimization?: any;
|
||||
content_calendar_optimization?: any;
|
||||
onboarding_data_used?: any;
|
||||
data_confidence_scores?: any;
|
||||
recommendation_quality_scores?: any;
|
||||
processing_time?: number;
|
||||
ai_service_status: string;
|
||||
prompt_version?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface OnboardingIntegration {
|
||||
id: string;
|
||||
user_id: number;
|
||||
strategy_id: string;
|
||||
website_analysis_data?: any;
|
||||
research_preferences_data?: any;
|
||||
api_keys_data?: any;
|
||||
field_mappings?: any;
|
||||
auto_populated_fields?: any;
|
||||
user_overrides?: any;
|
||||
data_quality_scores?: any;
|
||||
confidence_levels?: any;
|
||||
data_freshness?: any;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface StrategicInputField {
|
||||
id: string;
|
||||
category: string;
|
||||
label: string;
|
||||
description: string;
|
||||
tooltip: string;
|
||||
type: 'text' | 'number' | 'select' | 'multiselect' | 'json' | 'boolean';
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
placeholder?: string;
|
||||
validation?: any;
|
||||
auto_populated?: boolean;
|
||||
data_source?: string;
|
||||
confidence_level?: number;
|
||||
}
|
||||
|
||||
export interface ProgressiveDisclosureStep {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
fields: string[];
|
||||
is_complete: boolean;
|
||||
is_visible: boolean;
|
||||
dependencies: string[];
|
||||
}
|
||||
|
||||
export interface TooltipData {
|
||||
field_id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
examples: string[];
|
||||
best_practices: string[];
|
||||
data_source?: string;
|
||||
confidence_level?: number;
|
||||
}
|
||||
|
||||
// Store interface
|
||||
interface EnhancedStrategyStore {
|
||||
// State
|
||||
strategies: EnhancedStrategy[];
|
||||
currentStrategy: EnhancedStrategy | null;
|
||||
aiAnalyses: EnhancedAIAnalysis[];
|
||||
onboardingIntegrations: OnboardingIntegration[];
|
||||
|
||||
// Progressive Disclosure
|
||||
disclosureSteps: ProgressiveDisclosureStep[];
|
||||
currentStep: number;
|
||||
completedSteps: string[];
|
||||
|
||||
// Tooltips
|
||||
tooltips: Record<string, TooltipData>;
|
||||
|
||||
// Form State
|
||||
formData: Record<string, any>;
|
||||
formErrors: Record<string, string>;
|
||||
autoPopulatedFields: Record<string, any>;
|
||||
dataSources: Record<string, string>;
|
||||
|
||||
// UI State
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
saving: boolean;
|
||||
aiGenerating: boolean;
|
||||
|
||||
// Actions
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setSaving: (saving: boolean) => void;
|
||||
setAIGenerating: (generating: boolean) => void;
|
||||
|
||||
// Strategy actions
|
||||
createEnhancedStrategy: (strategy: Partial<EnhancedStrategy>) => Promise<EnhancedStrategy>;
|
||||
updateEnhancedStrategy: (id: string, updates: Partial<EnhancedStrategy>) => Promise<void>;
|
||||
deleteEnhancedStrategy: (id: string) => Promise<void>;
|
||||
setCurrentStrategy: (strategy: EnhancedStrategy | null) => void;
|
||||
|
||||
// Form actions
|
||||
updateFormField: (fieldId: string, value: any) => void;
|
||||
validateFormField: (fieldId: string) => boolean;
|
||||
validateAllFields: () => boolean;
|
||||
resetForm: () => void;
|
||||
|
||||
// Progressive disclosure actions
|
||||
setCurrentStep: (step: number) => void;
|
||||
completeStep: (stepId: string) => void;
|
||||
canProceedToStep: (stepId: string) => boolean;
|
||||
getNextStep: () => ProgressiveDisclosureStep | null;
|
||||
getPreviousStep: () => ProgressiveDisclosureStep | null;
|
||||
|
||||
// Auto-population actions
|
||||
autoPopulateFromOnboarding: () => Promise<void>;
|
||||
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => void;
|
||||
overrideAutoPopulatedField: (fieldId: string, value: any) => void;
|
||||
|
||||
// AI Analysis actions
|
||||
generateAIRecommendations: (strategyId: string) => Promise<void>;
|
||||
regenerateAIAnalysis: (strategyId: string, analysisType: string) => Promise<void>;
|
||||
|
||||
// Data loading
|
||||
loadEnhancedStrategies: () => Promise<void>;
|
||||
loadAIAnalyses: (strategyId: string) => Promise<void>;
|
||||
loadOnboardingIntegration: (strategyId: string) => Promise<void>;
|
||||
|
||||
// Tooltip actions
|
||||
getTooltipData: (fieldId: string) => TooltipData | null;
|
||||
updateTooltipData: (fieldId: string, data: TooltipData) => void;
|
||||
|
||||
// Completion tracking
|
||||
calculateCompletionPercentage: () => number;
|
||||
getCompletionStats: () => {
|
||||
total_fields: number;
|
||||
filled_fields: number;
|
||||
completion_percentage: number;
|
||||
category_completion: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
// Strategic input fields configuration
|
||||
export const STRATEGIC_INPUT_FIELDS: StrategicInputField[] = [
|
||||
// Business Context
|
||||
{
|
||||
id: 'business_objectives',
|
||||
category: 'business_context',
|
||||
label: 'Business Objectives',
|
||||
description: 'Primary and secondary business goals for content strategy',
|
||||
tooltip: 'Define your main business goals that content will support. Include both primary objectives (e.g., brand awareness) and secondary objectives (e.g., lead generation).',
|
||||
type: 'json',
|
||||
required: true,
|
||||
placeholder: 'Enter your business objectives'
|
||||
},
|
||||
{
|
||||
id: 'target_metrics',
|
||||
category: 'business_context',
|
||||
label: 'Target Metrics',
|
||||
description: 'KPIs and success metrics for content performance',
|
||||
tooltip: 'Specify the key performance indicators (KPIs) that will measure the success of your content strategy. Include metrics like traffic growth, engagement rates, and conversion rates.',
|
||||
type: 'json',
|
||||
required: true,
|
||||
placeholder: 'Define your target metrics'
|
||||
},
|
||||
{
|
||||
id: 'content_budget',
|
||||
category: 'business_context',
|
||||
label: 'Content Budget',
|
||||
description: 'Monthly or annual budget for content creation',
|
||||
tooltip: 'Set your content marketing budget. This helps determine the scope and scale of your content strategy, including team size, tools, and content production capabilities.',
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: 'Enter your content budget'
|
||||
},
|
||||
{
|
||||
id: 'team_size',
|
||||
category: 'business_context',
|
||||
label: 'Team Size',
|
||||
description: 'Number of people working on content',
|
||||
tooltip: 'Specify the size of your content team. This affects content production capacity and helps determine realistic content frequency and volume.',
|
||||
type: 'number',
|
||||
required: false,
|
||||
placeholder: 'Enter team size'
|
||||
},
|
||||
{
|
||||
id: 'implementation_timeline',
|
||||
category: 'business_context',
|
||||
label: 'Implementation Timeline',
|
||||
description: 'Timeline for strategy implementation',
|
||||
tooltip: 'Define how long you plan to implement this content strategy. Common timelines include 3 months, 6 months, or 1 year.',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: ['3 months', '6 months', '1 year', '2 years', 'Ongoing']
|
||||
},
|
||||
{
|
||||
id: 'market_share',
|
||||
category: 'business_context',
|
||||
label: 'Market Share',
|
||||
description: 'Current market share percentage',
|
||||
tooltip: 'Your current market share helps determine your competitive position and content strategy approach. Leaders focus on thought leadership, while challengers focus on differentiation.',
|
||||
type: 'text',
|
||||
required: false,
|
||||
placeholder: 'Enter market share percentage'
|
||||
},
|
||||
{
|
||||
id: 'competitive_position',
|
||||
category: 'business_context',
|
||||
label: 'Competitive Position',
|
||||
description: 'Market position relative to competitors',
|
||||
tooltip: 'Define your competitive position in the market. Options include Leader, Challenger, Niche, or Emerging. This influences your content strategy approach.',
|
||||
type: 'select',
|
||||
required: false,
|
||||
options: ['Leader', 'Challenger', 'Niche', 'Emerging']
|
||||
},
|
||||
{
|
||||
id: 'performance_metrics',
|
||||
category: 'business_context',
|
||||
label: 'Current Performance Metrics',
|
||||
description: 'Existing performance data and benchmarks',
|
||||
tooltip: 'Provide your current content performance metrics as a baseline. This helps measure improvement and set realistic targets.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Enter current performance data'
|
||||
},
|
||||
|
||||
// Audience Intelligence
|
||||
{
|
||||
id: 'content_preferences',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Content Preferences',
|
||||
description: 'Preferred content formats and topics',
|
||||
tooltip: 'Identify what types of content your audience prefers. Consider formats (blog posts, videos, infographics) and topics that resonate most.',
|
||||
type: 'json',
|
||||
required: true,
|
||||
placeholder: 'Define content preferences'
|
||||
},
|
||||
{
|
||||
id: 'consumption_patterns',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Consumption Patterns',
|
||||
description: 'When and how audience consumes content',
|
||||
tooltip: 'Understand when and how your audience consumes content. This includes peak times, preferred devices, and consumption channels.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Describe consumption patterns'
|
||||
},
|
||||
{
|
||||
id: 'audience_pain_points',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Audience Pain Points',
|
||||
description: 'Key challenges and pain points',
|
||||
tooltip: 'Identify the main challenges and pain points your audience faces. This helps create content that addresses real needs and provides value.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'List audience pain points'
|
||||
},
|
||||
{
|
||||
id: 'buying_journey',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Buying Journey',
|
||||
description: 'Customer journey stages and touchpoints',
|
||||
tooltip: 'Map your audience\'s buying journey stages and the content touchpoints that influence their decisions.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define buying journey stages'
|
||||
},
|
||||
{
|
||||
id: 'seasonal_trends',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Seasonal Trends',
|
||||
description: 'Seasonal content opportunities',
|
||||
tooltip: 'Identify seasonal trends and opportunities that affect your audience\'s content consumption and needs.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define seasonal trends'
|
||||
},
|
||||
{
|
||||
id: 'engagement_metrics',
|
||||
category: 'audience_intelligence',
|
||||
label: 'Engagement Metrics',
|
||||
description: 'Current engagement data',
|
||||
tooltip: 'Provide current engagement metrics to understand what content resonates with your audience.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Enter engagement metrics'
|
||||
},
|
||||
|
||||
// Competitive Intelligence
|
||||
{
|
||||
id: 'top_competitors',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Top Competitors',
|
||||
description: 'List of main competitors',
|
||||
tooltip: 'Identify your main competitors in the market. This helps understand competitive landscape and identify content opportunities.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'List top competitors'
|
||||
},
|
||||
{
|
||||
id: 'competitor_content_strategies',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Competitor Content Strategies',
|
||||
description: 'Analysis of competitor approaches',
|
||||
tooltip: 'Analyze your competitors\' content strategies to identify gaps, opportunities, and differentiation possibilities.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Analyze competitor strategies'
|
||||
},
|
||||
{
|
||||
id: 'market_gaps',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Market Gaps',
|
||||
description: 'Identified market opportunities',
|
||||
tooltip: 'Identify gaps in the market that your content can address. These are opportunities where competitors are not providing adequate content.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Identify market gaps'
|
||||
},
|
||||
{
|
||||
id: 'industry_trends',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Industry Trends',
|
||||
description: 'Current industry trends',
|
||||
tooltip: 'Stay current with industry trends that affect your audience and content strategy.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'List industry trends'
|
||||
},
|
||||
{
|
||||
id: 'emerging_trends',
|
||||
category: 'competitive_intelligence',
|
||||
label: 'Emerging Trends',
|
||||
description: 'Upcoming trends and opportunities',
|
||||
tooltip: 'Identify emerging trends that could provide early-mover advantages in content creation.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Identify emerging trends'
|
||||
},
|
||||
|
||||
// Content Strategy
|
||||
{
|
||||
id: 'preferred_formats',
|
||||
category: 'content_strategy',
|
||||
label: 'Preferred Formats',
|
||||
description: 'Content formats to focus on',
|
||||
tooltip: 'Choose the content formats that align with your audience preferences and business objectives.',
|
||||
type: 'multiselect',
|
||||
required: true,
|
||||
options: ['Blog Posts', 'Videos', 'Infographics', 'Webinars', 'Podcasts', 'Case Studies', 'Whitepapers', 'Social Media Posts']
|
||||
},
|
||||
{
|
||||
id: 'content_mix',
|
||||
category: 'content_strategy',
|
||||
label: 'Content Mix',
|
||||
description: 'Distribution of content types',
|
||||
tooltip: 'Define the percentage distribution of different content types in your strategy.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define content mix percentages'
|
||||
},
|
||||
{
|
||||
id: 'content_frequency',
|
||||
category: 'content_strategy',
|
||||
label: 'Content Frequency',
|
||||
description: 'How often to publish content',
|
||||
tooltip: 'Set realistic content publishing frequency based on your team capacity and audience expectations.',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: ['Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'Quarterly']
|
||||
},
|
||||
{
|
||||
id: 'optimal_timing',
|
||||
category: 'content_strategy',
|
||||
label: 'Optimal Timing',
|
||||
description: 'Best times for publishing',
|
||||
tooltip: 'Identify the optimal times for publishing different types of content to maximize engagement.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define optimal publishing times'
|
||||
},
|
||||
{
|
||||
id: 'quality_metrics',
|
||||
category: 'content_strategy',
|
||||
label: 'Quality Metrics',
|
||||
description: 'Content quality standards',
|
||||
tooltip: 'Define the quality standards and metrics that will ensure your content meets your audience\'s expectations.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define quality standards'
|
||||
},
|
||||
{
|
||||
id: 'editorial_guidelines',
|
||||
category: 'content_strategy',
|
||||
label: 'Editorial Guidelines',
|
||||
description: 'Style and tone guidelines',
|
||||
tooltip: 'Establish editorial guidelines for consistent brand voice, tone, and style across all content.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define editorial guidelines'
|
||||
},
|
||||
{
|
||||
id: 'brand_voice',
|
||||
category: 'content_strategy',
|
||||
label: 'Brand Voice',
|
||||
description: 'Brand personality and voice',
|
||||
tooltip: 'Define your brand\'s personality and voice characteristics to ensure consistent messaging.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define brand voice'
|
||||
},
|
||||
|
||||
// Performance & Analytics
|
||||
{
|
||||
id: 'traffic_sources',
|
||||
category: 'performance_analytics',
|
||||
label: 'Traffic Sources',
|
||||
description: 'Primary traffic sources',
|
||||
tooltip: 'Identify your main traffic sources to understand where your audience comes from and optimize accordingly.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define traffic sources'
|
||||
},
|
||||
{
|
||||
id: 'conversion_rates',
|
||||
category: 'performance_analytics',
|
||||
label: 'Conversion Rates',
|
||||
description: 'Current conversion data',
|
||||
tooltip: 'Track conversion rates across different content types and channels to identify what drives results.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Enter conversion data'
|
||||
},
|
||||
{
|
||||
id: 'content_roi_targets',
|
||||
category: 'performance_analytics',
|
||||
label: 'Content ROI Targets',
|
||||
description: 'ROI goals and targets',
|
||||
tooltip: 'Set realistic ROI targets for your content marketing efforts to measure return on investment.',
|
||||
type: 'json',
|
||||
required: false,
|
||||
placeholder: 'Define ROI targets'
|
||||
},
|
||||
{
|
||||
id: 'ab_testing_capabilities',
|
||||
category: 'performance_analytics',
|
||||
label: 'A/B Testing Capabilities',
|
||||
description: 'A/B testing availability',
|
||||
tooltip: 'Indicate whether you have A/B testing capabilities to optimize content performance.',
|
||||
type: 'boolean',
|
||||
required: false
|
||||
}
|
||||
];
|
||||
|
||||
// Progressive disclosure steps
|
||||
const PROGRESSIVE_DISCLOSURE_STEPS: ProgressiveDisclosureStep[] = [
|
||||
{
|
||||
id: 'business_context',
|
||||
title: 'Business Context',
|
||||
description: 'Define your business objectives and context',
|
||||
fields: ['business_objectives', 'target_metrics', 'content_budget', 'team_size', 'implementation_timeline', 'market_share', 'competitive_position', 'performance_metrics'],
|
||||
is_complete: false,
|
||||
is_visible: true,
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 'audience_intelligence',
|
||||
title: 'Audience Intelligence',
|
||||
description: 'Understand your target audience',
|
||||
fields: ['content_preferences', 'consumption_patterns', 'audience_pain_points', 'buying_journey', 'seasonal_trends', 'engagement_metrics'],
|
||||
is_complete: false,
|
||||
is_visible: false,
|
||||
dependencies: ['business_context']
|
||||
},
|
||||
{
|
||||
id: 'competitive_intelligence',
|
||||
title: 'Competitive Intelligence',
|
||||
description: 'Analyze your competitive landscape',
|
||||
fields: ['top_competitors', 'competitor_content_strategies', 'market_gaps', 'industry_trends', 'emerging_trends'],
|
||||
is_complete: false,
|
||||
is_visible: false,
|
||||
dependencies: ['audience_intelligence']
|
||||
},
|
||||
{
|
||||
id: 'content_strategy',
|
||||
title: 'Content Strategy',
|
||||
description: 'Define your content approach',
|
||||
fields: ['preferred_formats', 'content_mix', 'content_frequency', 'optimal_timing', 'quality_metrics', 'editorial_guidelines', 'brand_voice'],
|
||||
is_complete: false,
|
||||
is_visible: false,
|
||||
dependencies: ['competitive_intelligence']
|
||||
},
|
||||
{
|
||||
id: 'performance_analytics',
|
||||
title: 'Performance & Analytics',
|
||||
description: 'Set up measurement and optimization',
|
||||
fields: ['traffic_sources', 'conversion_rates', 'content_roi_targets', 'ab_testing_capabilities'],
|
||||
is_complete: false,
|
||||
is_visible: false,
|
||||
dependencies: ['content_strategy']
|
||||
}
|
||||
];
|
||||
|
||||
// Store implementation
|
||||
export const useEnhancedStrategyStore = create<EnhancedStrategyStore>((set, get) => ({
|
||||
// Initial state
|
||||
strategies: [],
|
||||
currentStrategy: null,
|
||||
aiAnalyses: [],
|
||||
onboardingIntegrations: [],
|
||||
|
||||
// Progressive Disclosure
|
||||
disclosureSteps: PROGRESSIVE_DISCLOSURE_STEPS,
|
||||
currentStep: 0,
|
||||
completedSteps: [],
|
||||
|
||||
// Tooltips
|
||||
tooltips: {},
|
||||
|
||||
// Form State
|
||||
formData: {},
|
||||
formErrors: {},
|
||||
autoPopulatedFields: {},
|
||||
dataSources: {},
|
||||
|
||||
// UI State
|
||||
loading: false,
|
||||
error: null,
|
||||
saving: false,
|
||||
aiGenerating: false,
|
||||
|
||||
// Actions
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setSaving: (saving) => set({ saving }),
|
||||
setAIGenerating: (generating) => set({ aiGenerating: generating }),
|
||||
|
||||
// Strategy actions
|
||||
createEnhancedStrategy: async (strategy) => {
|
||||
set({ saving: true, error: null });
|
||||
try {
|
||||
const newStrategy = await contentPlanningApi.createEnhancedStrategy(strategy);
|
||||
set((state) => ({
|
||||
strategies: [...state.strategies, newStrategy],
|
||||
saving: false,
|
||||
}));
|
||||
return newStrategy; // Return the created strategy
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to create enhanced strategy', saving: false });
|
||||
throw error; // Re-throw the error so the calling function can handle it
|
||||
}
|
||||
},
|
||||
|
||||
updateEnhancedStrategy: async (id, updates) => {
|
||||
set({ saving: true, error: null });
|
||||
try {
|
||||
const updatedStrategy = await contentPlanningApi.updateEnhancedStrategy(id, updates);
|
||||
set((state) => ({
|
||||
strategies: state.strategies.map((strategy) =>
|
||||
strategy.id === id ? updatedStrategy : strategy
|
||||
),
|
||||
saving: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to update enhanced strategy', saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
deleteEnhancedStrategy: async (id) => {
|
||||
set({ saving: true, error: null });
|
||||
try {
|
||||
await contentPlanningApi.deleteEnhancedStrategy(id);
|
||||
set((state) => ({
|
||||
strategies: state.strategies.filter((strategy) => strategy.id !== id),
|
||||
saving: false,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to delete enhanced strategy', saving: false });
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentStrategy: (strategy) => set({ currentStrategy: strategy }),
|
||||
|
||||
// Form actions
|
||||
updateFormField: (fieldId, value) => {
|
||||
set((state) => ({
|
||||
formData: { ...state.formData, [fieldId]: value },
|
||||
formErrors: { ...state.formErrors, [fieldId]: '' }
|
||||
}));
|
||||
},
|
||||
|
||||
validateFormField: (fieldId) => {
|
||||
const field = STRATEGIC_INPUT_FIELDS.find(f => f.id === fieldId);
|
||||
if (!field) return true;
|
||||
|
||||
const value = get().formData[fieldId];
|
||||
|
||||
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
|
||||
set((state) => ({
|
||||
formErrors: { ...state.formErrors, [fieldId]: `${field.label} is required` }
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateAllFields: () => {
|
||||
const { formData } = get();
|
||||
let isValid = true;
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
STRATEGIC_INPUT_FIELDS.forEach(field => {
|
||||
const value = formData[field.id];
|
||||
|
||||
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
|
||||
errors[field.id] = `${field.label} is required`;
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
set({ formErrors: errors });
|
||||
return isValid;
|
||||
},
|
||||
|
||||
resetForm: () => {
|
||||
set({
|
||||
formData: {},
|
||||
formErrors: {},
|
||||
autoPopulatedFields: {},
|
||||
dataSources: {},
|
||||
currentStep: 0,
|
||||
completedSteps: []
|
||||
});
|
||||
},
|
||||
|
||||
// Progressive disclosure actions
|
||||
setCurrentStep: (step) => set({ currentStep: step }),
|
||||
|
||||
completeStep: (stepId) => {
|
||||
set((state) => ({
|
||||
completedSteps: [...state.completedSteps, stepId],
|
||||
disclosureSteps: state.disclosureSteps.map(step =>
|
||||
step.id === stepId ? { ...step, is_complete: true } : step
|
||||
)
|
||||
}));
|
||||
},
|
||||
|
||||
canProceedToStep: (stepId) => {
|
||||
const { disclosureSteps, completedSteps } = get();
|
||||
const step = disclosureSteps.find(s => s.id === stepId);
|
||||
if (!step) return false;
|
||||
|
||||
return step.dependencies.every(dep => completedSteps.includes(dep));
|
||||
},
|
||||
|
||||
getNextStep: () => {
|
||||
const { disclosureSteps, currentStep } = get();
|
||||
const nextStep = disclosureSteps[currentStep + 1];
|
||||
return nextStep && get().canProceedToStep(nextStep.id) ? nextStep : null;
|
||||
},
|
||||
|
||||
getPreviousStep: () => {
|
||||
const { disclosureSteps, currentStep } = get();
|
||||
return currentStep > 0 ? disclosureSteps[currentStep - 1] : null;
|
||||
},
|
||||
|
||||
// Auto-population actions
|
||||
autoPopulateFromOnboarding: async () => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
console.log('🔄 Starting auto-population from onboarding data...');
|
||||
|
||||
// This would call the backend to get onboarding data and auto-populate fields
|
||||
const response = await contentPlanningApi.getOnboardingData();
|
||||
console.log('📡 Backend response:', response);
|
||||
|
||||
// Extract field values and sources from the new backend format
|
||||
const fields = response.data?.fields || {};
|
||||
const sources = response.data?.sources || {};
|
||||
|
||||
console.log('📋 Extracted fields:', fields);
|
||||
console.log('🔗 Data sources:', sources);
|
||||
|
||||
// Transform the fields object to extract values for formData
|
||||
const fieldValues: Record<string, any> = {};
|
||||
const autoPopulatedFields: Record<string, any> = {};
|
||||
|
||||
Object.keys(fields).forEach(fieldId => {
|
||||
const fieldData = fields[fieldId];
|
||||
console.log(`🔍 Processing field ${fieldId}:`, fieldData);
|
||||
|
||||
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
||||
fieldValues[fieldId] = fieldData.value;
|
||||
autoPopulatedFields[fieldId] = fieldData.value;
|
||||
console.log(`✅ Auto-populated ${fieldId}:`, fieldData.value);
|
||||
} else {
|
||||
console.log(`❌ Skipping ${fieldId} - invalid data structure`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📝 Final field values:', fieldValues);
|
||||
console.log('🔄 Final auto-populated fields:', autoPopulatedFields);
|
||||
|
||||
set((state) => ({
|
||||
autoPopulatedFields,
|
||||
dataSources: sources,
|
||||
formData: { ...state.formData, ...fieldValues }
|
||||
}));
|
||||
|
||||
console.log('✅ Auto-population completed successfully');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Auto-population error:', error);
|
||||
set({ error: error.message || 'Failed to auto-populate from onboarding' });
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateAutoPopulatedField: (fieldId, value, source) => {
|
||||
set((state) => ({
|
||||
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value },
|
||||
dataSources: { ...state.dataSources, [fieldId]: source }
|
||||
}));
|
||||
},
|
||||
|
||||
overrideAutoPopulatedField: (fieldId, value) => {
|
||||
set((state) => ({
|
||||
formData: { ...state.formData, [fieldId]: value },
|
||||
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value }
|
||||
}));
|
||||
},
|
||||
|
||||
// AI Analysis actions
|
||||
generateAIRecommendations: async (strategyId) => {
|
||||
set({ aiGenerating: true, error: null });
|
||||
try {
|
||||
const aiAnalysis = await contentPlanningApi.generateEnhancedAIRecommendations(strategyId);
|
||||
set((state) => ({
|
||||
aiAnalyses: [...state.aiAnalyses, aiAnalysis],
|
||||
aiGenerating: false
|
||||
}));
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to generate AI recommendations', aiGenerating: false });
|
||||
}
|
||||
},
|
||||
|
||||
regenerateAIAnalysis: async (strategyId: string, analysisType: string) => {
|
||||
set({ aiGenerating: true, error: null });
|
||||
try {
|
||||
const aiAnalysis = await contentPlanningApi.regenerateAIAnalysis(strategyId, analysisType);
|
||||
set((state) => ({
|
||||
aiAnalyses: state.aiAnalyses.map(analysis =>
|
||||
analysis.strategy_id === strategyId && analysis.analysis_type === analysisType
|
||||
? { ...analysis, ...aiAnalysis }
|
||||
: analysis
|
||||
),
|
||||
aiGenerating: false
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to regenerate AI analysis',
|
||||
aiGenerating: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Data loading
|
||||
loadEnhancedStrategies: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const strategies = await contentPlanningApi.getEnhancedStrategies();
|
||||
set({ strategies, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load enhanced strategies', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadAIAnalyses: async (strategyId) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const analyses = await contentPlanningApi.getEnhancedAIAnalyses(strategyId);
|
||||
set({ aiAnalyses: analyses, loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load AI analyses', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadOnboardingIntegration: async (strategyId) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const integration = await contentPlanningApi.getOnboardingIntegration(strategyId);
|
||||
set({ onboardingIntegrations: [integration], loading: false });
|
||||
} catch (error: any) {
|
||||
set({ error: error.message || 'Failed to load onboarding integration', loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Tooltip actions
|
||||
getTooltipData: (fieldId) => {
|
||||
const field = STRATEGIC_INPUT_FIELDS.find(f => f.id === fieldId);
|
||||
if (!field) return null;
|
||||
|
||||
const state = get();
|
||||
const autoPopulatedFields = state.autoPopulatedFields || {};
|
||||
const dataSources = state.dataSources || {};
|
||||
|
||||
return {
|
||||
field_id: fieldId,
|
||||
title: field.label,
|
||||
description: field.tooltip,
|
||||
examples: [],
|
||||
best_practices: [],
|
||||
data_source: dataSources[fieldId],
|
||||
confidence_level: autoPopulatedFields[fieldId] ? 0.8 : undefined
|
||||
};
|
||||
},
|
||||
|
||||
updateTooltipData: (fieldId, data) => {
|
||||
set((state) => ({
|
||||
tooltips: { ...state.tooltips, [fieldId]: data }
|
||||
}));
|
||||
},
|
||||
|
||||
// Completion tracking
|
||||
calculateCompletionPercentage: () => {
|
||||
const { formData } = get();
|
||||
const requiredFields = STRATEGIC_INPUT_FIELDS.filter(field => field.required);
|
||||
const filledRequiredFields = requiredFields.filter(field =>
|
||||
formData[field.id] &&
|
||||
(typeof formData[field.id] === 'string' ? formData[field.id].trim() !== '' : true)
|
||||
);
|
||||
|
||||
return (filledRequiredFields.length / requiredFields.length) * 100;
|
||||
},
|
||||
|
||||
getCompletionStats: () => {
|
||||
const { formData } = get();
|
||||
const categories = ['business_context', 'audience_intelligence', 'competitive_intelligence', 'content_strategy', 'performance_analytics'];
|
||||
|
||||
const category_completion: Record<string, number> = {};
|
||||
|
||||
categories.forEach(category => {
|
||||
const categoryFields = STRATEGIC_INPUT_FIELDS.filter(field => field.category === category);
|
||||
const filledFields = categoryFields.filter(field =>
|
||||
formData[field.id] &&
|
||||
(typeof formData[field.id] === 'string' ? formData[field.id].trim() !== '' : true)
|
||||
);
|
||||
|
||||
category_completion[category] = (filledFields.length / categoryFields.length) * 100;
|
||||
});
|
||||
|
||||
const total_fields = STRATEGIC_INPUT_FIELDS.length;
|
||||
const filled_fields = STRATEGIC_INPUT_FIELDS.filter(field =>
|
||||
formData[field.id] &&
|
||||
(typeof formData[field.id] === 'string' ? formData[field.id].trim() !== '' : true)
|
||||
).length;
|
||||
|
||||
return {
|
||||
total_fields,
|
||||
filled_fields,
|
||||
completion_percentage: (filled_fields / total_fields) * 100,
|
||||
category_completion
|
||||
};
|
||||
}
|
||||
}));
|
||||
9
frontend/src/stores/index.ts
Normal file
9
frontend/src/stores/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Export all stores
|
||||
export { useDashboardStore } from './dashboardStore';
|
||||
export { useSEODashboardStore } from './seoDashboardStore';
|
||||
export { useSharedDashboardStore } from './sharedDashboardStore';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { DashboardStore } from './dashboardStore';
|
||||
export type { SEODashboardStore } from './seoDashboardStore';
|
||||
export type { SharedDashboardState } from './sharedDashboardStore';
|
||||
174
frontend/src/stores/seoDashboardStore.ts
Normal file
174
frontend/src/stores/seoDashboardStore.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import { SEODashboardData } from '../api/seoDashboard';
|
||||
import { SEOAnalysisData } from '../components/shared/types';
|
||||
import { seoAnalysisAPI } from '../api/seoAnalysis';
|
||||
|
||||
export interface SEODashboardStore {
|
||||
// State
|
||||
data: SEODashboardData | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
analysisData: SEOAnalysisData | null;
|
||||
analysisLoading: boolean;
|
||||
analysisError: string | null;
|
||||
hasRunInitialAnalysis: boolean;
|
||||
|
||||
// Actions
|
||||
setData: (data: SEODashboardData) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setAnalysisData: (data: SEOAnalysisData | null) => void;
|
||||
setAnalysisLoading: (loading: boolean) => void;
|
||||
setAnalysisError: (error: string | null) => void;
|
||||
runSEOAnalysis: () => Promise<void>;
|
||||
clearAnalysisError: () => void;
|
||||
checkAndRunInitialAnalysis: () => void;
|
||||
}
|
||||
|
||||
export const useSEODashboardStore = create<SEODashboardStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
analysisData: null,
|
||||
analysisLoading: false,
|
||||
analysisError: null,
|
||||
hasRunInitialAnalysis: false,
|
||||
|
||||
// Actions
|
||||
setData: (data) => set({ data }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setAnalysisData: (data) => set({ analysisData: data }),
|
||||
setAnalysisLoading: (loading) => set({ analysisLoading: loading }),
|
||||
setAnalysisError: (error) => set({ analysisError: error }),
|
||||
|
||||
clearAnalysisError: () => set({ analysisError: null }),
|
||||
|
||||
runSEOAnalysis: async () => {
|
||||
const currentData = get().data;
|
||||
|
||||
// Get URL from onboarding data or use a fallback
|
||||
let url = currentData?.website_url;
|
||||
|
||||
// If no URL from dashboard data, try to fetch from onboarding
|
||||
if (!url) {
|
||||
try {
|
||||
// Import the user data API to get user's website URL
|
||||
const { userDataAPI } = await import('../api/userData');
|
||||
const userData = await userDataAPI.getUserData();
|
||||
url = userData?.website_url || userData?.website_analysis?.website_url;
|
||||
console.log('Fetched URL from user data:', url);
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch URL from user data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// If still no URL, try the dedicated website URL endpoint
|
||||
if (!url) {
|
||||
try {
|
||||
const { userDataAPI } = await import('../api/userData');
|
||||
const websiteUrl = await userDataAPI.getWebsiteURL();
|
||||
if (websiteUrl) {
|
||||
url = websiteUrl;
|
||||
console.log('Fetched URL from dedicated endpoint:', url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch URL from dedicated endpoint:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback - only use if no URL was found from database
|
||||
if (!url) {
|
||||
url = 'https://example.com';
|
||||
console.warn('Using fallback URL:', url);
|
||||
}
|
||||
|
||||
console.log('Starting SEO analysis with URL:', url);
|
||||
console.log('Current store state:', get());
|
||||
|
||||
set({ analysisLoading: true, analysisError: null });
|
||||
|
||||
try {
|
||||
console.log(`Starting SEO analysis for URL: ${url}`);
|
||||
const result = await seoAnalysisAPI.analyzeURL(url);
|
||||
|
||||
console.log('API result received:', result);
|
||||
|
||||
if (result) {
|
||||
console.log('SEO analysis completed successfully:', result);
|
||||
set({
|
||||
analysisData: result,
|
||||
analysisLoading: false,
|
||||
hasRunInitialAnalysis: true
|
||||
});
|
||||
|
||||
console.log('Store state after setting analysis data:', get());
|
||||
|
||||
// Update main dashboard data based on analysis
|
||||
if (currentData) {
|
||||
const updatedData = {
|
||||
...currentData,
|
||||
health_score: {
|
||||
score: result.overall_score,
|
||||
change: 0,
|
||||
trend: 'stable',
|
||||
label: result.health_status.replace('_', ' ').toUpperCase(),
|
||||
color: result.health_status === 'poor' ? '#D32F2F' :
|
||||
result.health_status === 'needs_improvement' ? '#FF9800' : '#4CAF50'
|
||||
},
|
||||
key_insight: result.critical_issues.length > 0
|
||||
? `${result.critical_issues.length} critical issues found`
|
||||
: 'SEO analysis completed successfully',
|
||||
priority_alert: result.health_status === 'poor'
|
||||
? 'Immediate attention required'
|
||||
: result.health_status === 'needs_improvement'
|
||||
? 'Improvements recommended'
|
||||
: 'Good SEO health',
|
||||
website_url: url // Update the website URL with the actual URL used
|
||||
};
|
||||
set({ data: updatedData });
|
||||
}
|
||||
} else {
|
||||
console.error('Analysis returned null result');
|
||||
set({
|
||||
analysisError: 'Analysis failed to return results',
|
||||
analysisLoading: false
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('SEO Analysis error:', error);
|
||||
|
||||
let errorMessage = 'Analysis failed';
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
errorMessage = 'Analysis timed out. Please try again.';
|
||||
} else if (error.response?.status === 500) {
|
||||
errorMessage = 'Server error. Please try again later.';
|
||||
} else if (error.response?.status === 404) {
|
||||
errorMessage = 'Analysis service not found.';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
set({
|
||||
analysisError: errorMessage,
|
||||
analysisLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
checkAndRunInitialAnalysis: () => {
|
||||
const { analysisData, hasRunInitialAnalysis, data } = get();
|
||||
if (!analysisData && !hasRunInitialAnalysis && data) {
|
||||
get().runSEOAnalysis();
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'seo-dashboard-store',
|
||||
}
|
||||
)
|
||||
);
|
||||
100
frontend/src/stores/sharedDashboardStore.ts
Normal file
100
frontend/src/stores/sharedDashboardStore.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
|
||||
export interface SharedDashboardState {
|
||||
// Common state
|
||||
isSidebarOpen: boolean;
|
||||
currentTheme: 'light' | 'dark' | 'auto';
|
||||
notifications: Array<{
|
||||
id: string;
|
||||
message: string;
|
||||
type: 'info' | 'success' | 'warning' | 'error';
|
||||
timestamp: Date;
|
||||
read: boolean;
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
toggleSidebar: () => void;
|
||||
setSidebarOpen: (open: boolean) => void;
|
||||
setTheme: (theme: 'light' | 'dark' | 'auto') => void;
|
||||
addNotification: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void;
|
||||
markNotificationAsRead: (id: string) => void;
|
||||
clearNotifications: () => void;
|
||||
clearOldNotifications: () => void;
|
||||
}
|
||||
|
||||
export const useSharedDashboardStore = create<SharedDashboardState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
isSidebarOpen: false,
|
||||
currentTheme: 'auto',
|
||||
notifications: [],
|
||||
|
||||
// Actions
|
||||
toggleSidebar: () => {
|
||||
set((state) => ({ isSidebarOpen: !state.isSidebarOpen }));
|
||||
},
|
||||
|
||||
setSidebarOpen: (open: boolean) => {
|
||||
set({ isSidebarOpen: open });
|
||||
},
|
||||
|
||||
setTheme: (theme: 'light' | 'dark' | 'auto') => {
|
||||
set({ currentTheme: theme });
|
||||
// Apply theme to document
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (theme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
// Auto theme - check system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (prefersDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
addNotification: (message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') => {
|
||||
const notification = {
|
||||
id: Date.now().toString(),
|
||||
message,
|
||||
type,
|
||||
timestamp: new Date(),
|
||||
read: false,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
notifications: [notification, ...state.notifications].slice(0, 10), // Keep only last 10
|
||||
}));
|
||||
},
|
||||
|
||||
markNotificationAsRead: (id: string) => {
|
||||
set((state) => ({
|
||||
notifications: state.notifications.map((notification) =>
|
||||
notification.id === id ? { ...notification, read: true } : notification
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
clearNotifications: () => {
|
||||
set({ notifications: [] });
|
||||
},
|
||||
|
||||
clearOldNotifications: () => {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
set((state) => ({
|
||||
notifications: state.notifications.filter(
|
||||
(notification) => notification.timestamp > oneDayAgo
|
||||
),
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'shared-dashboard-store',
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user