ALwrity version 0.5.4

This commit is contained in:
ajaysi
2025-08-09 23:14:16 +05:30
parent 01fe1e0a9c
commit 5c08b6e007
42 changed files with 3514 additions and 2148 deletions

View File

@@ -71,6 +71,7 @@ import { getEducationalContent } from './ContentStrategyBuilder/utils/educationa
import CategoryList from './ContentStrategyBuilder/components/CategoryList';
import ProgressTracker from './ContentStrategyBuilder/components/ProgressTracker';
import HeaderSection from './ContentStrategyBuilder/components/HeaderSection';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const ContentStrategyBuilder: React.FC = () => {
const {
@@ -112,6 +113,10 @@ const ContentStrategyBuilder: React.FC = () => {
const [showEducationalInfo, setShowEducationalInfo] = useState<string | null>(null);
const [showAIRecommendations, setShowAIRecommendations] = useState(false);
const [showDataSourceTransparency, setShowDataSourceTransparency] = useState(false);
const [refreshMessage, setRefreshMessage] = useState<string | null>(null);
const [refreshProgress, setRefreshProgress] = useState<number>(0);
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [refreshError, setRefreshError] = useState<string | null>(null);
// Ref to track if we've already set the default category
const hasSetDefaultCategory = useRef(false);
@@ -310,8 +315,20 @@ const ContentStrategyBuilder: React.FC = () => {
{/* Error Alert */}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
<Alert
severity="error"
sx={{ mb: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
action={
<Box sx={{ display: 'flex', gap: 1 }}>
<Button size="small" variant="outlined" onClick={() => autoPopulateFromOnboarding(true)} startIcon={<RefreshIcon />}>Retry</Button>
<Button size="small" variant="contained" color="primary" onClick={() => setShowDataSourceTransparency(true)} startIcon={<InfoIcon />}>Why?</Button>
</Box>
}
>
<Box>
<Typography variant="subtitle2">Real data required</Typography>
<Typography variant="body2">{error || 'We could not auto-populate because required onboarding/analysis data is missing. Connect sources or complete onboarding, then retry.'}</Typography>
</Box>
</Alert>
)}
@@ -380,7 +397,87 @@ const ContentStrategyBuilder: React.FC = () => {
aiGenerating={aiGenerating}
onShowAIRecommendations={() => setShowAIRecommendations(true)}
onShowDataSourceTransparency={() => setShowDataSourceTransparency(true)}
onRefreshData={autoPopulateFromOnboarding}
onRefreshData={() => autoPopulateFromOnboarding()}
onRefreshAI={async () => {
try {
setAIGenerating(true);
setIsRefreshing(true);
setRefreshError(null);
setRefreshMessage('Initializing refresh…');
setRefreshProgress(5);
const es = await contentPlanningApi.streamAutofillRefresh(1, true, true);
es.onmessage = (evt: MessageEvent) => {
try {
const data = JSON.parse(evt.data);
if (data.type === 'status' || data.type === 'progress') {
setRefreshMessage(data.message || 'Refreshing…');
if (typeof data.progress === 'number') setRefreshProgress(data.progress);
}
if (data.type === 'result') {
const payload = data.data || {};
const fields = payload.fields || {};
const sources = payload.sources || {};
const inputDataPoints = payload.input_data_points || {};
const meta = payload.meta || {};
const fieldValues: Record<string, any> = {};
Object.keys(fields).forEach((fieldId) => {
const fieldData = fields[fieldId];
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
fieldValues[fieldId] = fieldData.value;
}
});
useEnhancedStrategyStore.setState((state) => ({
autoPopulatedFields: { ...state.autoPopulatedFields, ...fieldValues },
dataSources: { ...state.dataSources, ...sources },
inputDataPoints,
formData: { ...state.formData, ...fieldValues }
}));
if (!meta.ai_used || meta.ai_overrides_count === 0) {
const msg = 'AI did not produce new values. Please try again or complete onboarding data.';
setError(msg);
setRefreshError(msg);
setRefreshMessage('No new AI values available.');
}
es.close();
setAIGenerating(false);
setIsRefreshing(false);
if (!meta || meta.ai_overrides_count > 0) {
setRefreshMessage(null);
setRefreshProgress(0);
}
}
if (data.type === 'error') {
const msg = data.message || 'AI refresh failed.';
setRefreshError(msg);
es.close();
setAIGenerating(false);
setIsRefreshing(false);
setRefreshMessage('Refresh failed.');
}
} catch (err: any) {
console.error('SSE parse error:', err);
}
};
es.onerror = (err: any) => {
console.error('SSE connection error:', err);
es.close();
setAIGenerating(false);
setIsRefreshing(false);
setRefreshError('AI refresh connection lost. Please try again.');
setRefreshMessage('Connection lost.');
};
} catch (e) {
console.error('AI refresh error', e);
setAIGenerating(false);
setIsRefreshing(false);
setRefreshError('AI refresh failed. Please try again.');
setRefreshMessage('Refresh failed.');
}
}}
refreshMessage={refreshMessage}
refreshProgress={refreshProgress}
isRefreshing={isRefreshing}
refreshError={refreshError}
/>
{/* Category Progress - Compact with Futuristic Styling */}
@@ -428,7 +525,7 @@ const ContentStrategyBuilder: React.FC = () => {
size="small"
variant="outlined"
startIcon={<RefreshIcon />}
onClick={autoPopulateFromOnboarding}
onClick={() => autoPopulateFromOnboarding(true)}
fullWidth
>
Refresh Data
@@ -518,8 +615,8 @@ const ContentStrategyBuilder: React.FC = () => {
{/* Category Fields */}
<Box sx={{ mt: 1 }}>
<Grid container spacing={2}>
{STRATEGIC_INPUT_FIELDS
.filter(field => field.category === activeCategory)
{STRATEGIC_INPUT_FIELDS
.filter(field => field.category === activeCategory)
.map((field, index) => {
// Determine grid size based on field type for better layout organization
const type = field.type;
@@ -531,30 +628,30 @@ const ContentStrategyBuilder: React.FC = () => {
const gridMd = forceFullWidth ? 12 : (isWideField ? 12 : isMediumField ? 6 : 4);
const gridLg = forceFullWidth ? 12 : (isWideField ? 12 : isMediumField ? 6 : 4);
const gridSm = 12;
return (
return (
<Grid item xs={12} sm={gridSm} md={gridMd} lg={gridLg} key={field.id}>
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25, delay: index * 0.03 }}>
<StrategicInputField
fieldId={field.id}
value={formData[field.id]}
error={formErrors[field.id]}
autoPopulated={!!autoPopulatedFields[field.id]}
dataSource={dataSources[field.id]}
confidenceLevel={autoPopulatedFields[field.id] ? 0.8 : undefined}
dataQuality={autoPopulatedFields[field.id] ? 'High Quality' : undefined}
onChange={(value: any) => updateFormField(field.id, value)}
onValidate={() => validateFormField(field.id)}
onShowTooltip={() => setShowTooltip(field.id)}
<StrategicInputField
fieldId={field.id}
value={formData[field.id]}
error={formErrors[field.id]}
autoPopulated={!!autoPopulatedFields[field.id]}
dataSource={dataSources[field.id]}
confidenceLevel={autoPopulatedFields[field.id] ? 0.8 : undefined}
dataQuality={autoPopulatedFields[field.id] ? 'High Quality' : undefined}
onChange={(value: any) => updateFormField(field.id, value)}
onValidate={() => validateFormField(field.id)}
onShowTooltip={() => setShowTooltip(field.id)}
onViewDataSource={() => setShowDataSourceTransparency(true)}
accentColorKey={getCategoryColor(activeCategory) as any}
isCompact={isCompactField}
/>
/>
</motion.div>
</Grid>
);
})}
</Grid>
</Grid>
);
})}
</Grid>
</Box>
{/* Category Actions */}
@@ -567,26 +664,26 @@ const ContentStrategyBuilder: React.FC = () => {
reviewedCategories: Array.from(reviewedCategories)
});
return !isReviewed ? (
<Button
variant="contained"
<Button
variant="contained"
onClick={() => {
console.log('🔘 Button clicked! activeCategory:', activeCategory);
console.log('🔘 reviewedCategories:', Array.from(reviewedCategories));
console.log('🔘 isMarkingReviewed:', isMarkingReviewed);
handleConfirmCategoryReviewWrapper();
}}
startIcon={isMarkingReviewed ? <CircularProgress size={20} /> : <CheckCircleIcon />}
disabled={isMarkingReviewed}
>
{isMarkingReviewed ? 'Marking as Reviewed...' : 'Mark as Reviewed'}
</Button>
) : (
<Chip
label="Category Reviewed"
color="success"
icon={<CheckCircleIcon />}
sx={{ px: 2, py: 1 }}
/>
startIcon={isMarkingReviewed ? <CircularProgress size={20} /> : <CheckCircleIcon />}
disabled={isMarkingReviewed}
>
{isMarkingReviewed ? 'Marking as Reviewed...' : 'Mark as Reviewed'}
</Button>
) : (
<Chip
label="Category Reviewed"
color="success"
icon={<CheckCircleIcon />}
sx={{ px: 2, py: 1 }}
/>
);
})()}

View File

@@ -24,6 +24,12 @@ interface ProgressTrackerProps {
onShowAIRecommendations: () => void;
onShowDataSourceTransparency: () => void;
onRefreshData: () => void;
onRefreshAI?: () => void;
// New optional props for refresh feedback
refreshMessage?: string | null;
refreshProgress?: number;
isRefreshing?: boolean;
refreshError?: string | null;
}
const ProgressTracker: React.FC<ProgressTrackerProps> = ({
@@ -34,28 +40,25 @@ const ProgressTracker: React.FC<ProgressTrackerProps> = ({
aiGenerating,
onShowAIRecommendations,
onShowDataSourceTransparency,
onRefreshData
onRefreshData,
onRefreshAI,
refreshMessage,
refreshProgress = 0,
isRefreshing = false,
refreshError = null
}) => {
const effectiveProgress = isRefreshing ? Math.max(5, Math.min(100, Math.round(refreshProgress))) : Math.round(reviewProgressPercentage);
return (
<Box sx={{ mb: 1.5 }}>
{/* Compact header row with title, progress, counts and actions */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 0.75 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" sx={{ mb: 0, fontSize: '1rem' }}>
Progress
</Typography>
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<CircularProgress
variant="determinate"
value={reviewProgressPercentage}
size={28}
thickness={4}
sx={{ color: 'primary.main', '& .MuiCircularProgress-circle': { strokeLinecap: 'round' } }}
/>
<Box sx={{ top: 0, left: 0, bottom: 0, right: 0, position: 'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="caption" component="div" color="text.secondary" sx={{ fontSize: '0.65rem', fontWeight: 700 }}>
{`${Math.round(reviewProgressPercentage)}%`}
</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>Progress</Typography>
<Box sx={{ position: 'relative', width: 28, height: 28 }}>
<CircularProgress variant="determinate" value={effectiveProgress} size={28} thickness={5} />
<Box sx={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="caption" sx={{ fontSize: 10 }}>{effectiveProgress}%</Typography>
</Box>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
@@ -64,49 +67,44 @@ const ProgressTracker: React.FC<ProgressTrackerProps> = ({
</Box>
{/* Actions inline in header */}
<Box sx={{ display: 'flex', gap: 0.75 }}>
<MuiTooltip title="View AI-powered recommendations and insights" placement="top">
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
onClick={onShowAIRecommendations}
sx={{ color: 'primary.main', bgcolor: 'rgba(255, 193, 7, 0.08)', border: '1px solid rgba(255, 193, 7, 0.25)', width: 32, height: 32, '&:hover': { bgcolor: 'rgba(255, 193, 7, 0.16)' } }}
>
<Badge badgeContent={5} sx={{ '& .MuiBadge-badge': { fontSize: '0.55rem', fontWeight: 700, bgcolor: '#ff6b35', color: 'white' } }}>
<AutoAwesomeIcon sx={{ fontSize: 16 }} />
</Badge>
</IconButton>
</motion.div>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<MuiTooltip title="AI Recommendations">
<IconButton size="small" onClick={onShowAIRecommendations}>
<AutoAwesomeIcon fontSize="small" />
</IconButton>
</MuiTooltip>
<MuiTooltip title="View data sources and transparency information" placement="top">
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
onClick={onShowDataSourceTransparency}
sx={{ color: 'primary.main', bgcolor: 'rgba(76, 175, 80, 0.08)', border: '1px solid rgba(76, 175, 80, 0.25)', width: 32, height: 32, '&:hover': { bgcolor: 'rgba(76, 175, 80, 0.16)' } }}
>
<Badge badgeContent={Object.keys(autoPopulatedFields || {}).length} sx={{ '& .MuiBadge-badge': { fontSize: '0.55rem', fontWeight: 700, bgcolor: '#2196f3', color: 'white' } }}>
<InfoIcon sx={{ fontSize: 16 }} />
</Badge>
</IconButton>
</motion.div>
<MuiTooltip title="Data Transparency">
<IconButton size="small" onClick={onShowDataSourceTransparency}>
<InfoIcon fontSize="small" />
</IconButton>
</MuiTooltip>
<MuiTooltip title="Refresh auto-populated data" placement="top">
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton onClick={onRefreshData} sx={{ color: 'primary.main', bgcolor: 'rgba(0,0,0,0.04)', border: '1px solid rgba(0,0,0,0.12)', width: 32, height: 32, '&:hover': { bgcolor: 'rgba(0,0,0,0.08)' } }}>
<RefreshIcon sx={{ fontSize: 16 }} />
</IconButton>
</motion.div>
<MuiTooltip title="Refresh Data (AI)">
<IconButton size="small" onClick={onRefreshAI || onRefreshData}>
<RefreshIcon fontSize="small" />
</IconButton>
</MuiTooltip>
</Box>
</Box>
{/* Combined info line */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Combined info line with refresh/error banner */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minHeight: 22 }}>
<CheckCircleIcon color="success" sx={{ fontSize: 14 }} />
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
Auto-population: {Object.keys(autoPopulatedFields || {}).length} fields AI Insights: {aiGenerating ? 'Generating...' : 'Ready'}
</Typography>
{refreshError ? (
<Typography variant="caption" color="error" sx={{ fontSize: '0.72rem' }}>
{refreshError}
</Typography>
) : isRefreshing ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<CircularProgress size={12} thickness={6} />
<Typography variant="caption" color="primary" sx={{ fontSize: '0.72rem' }}>
{refreshMessage || 'Refreshing data…'}
</Typography>
</Box>
) : (
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
Auto-population: {Object.keys(autoPopulatedFields || {}).length} fields AI Insights: {aiGenerating ? 'Generating…' : 'Ready'}
</Typography>
)}
</Box>
</Box>
);

View File

@@ -658,6 +658,36 @@ class ContentPlanningAPI {
return new EventSource(url);
}
// Clear enhanced strategy streaming/cache for a user (best-effort refresh)
async clearEnhancedCache(userId?: number): Promise<any> {
const params: any = {};
if (userId) params.user_id = userId;
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/cache/clear`, null, { params });
return response.data;
}
// Stream AI generation/status updates for a specific strategy (best-effort)
async streamAIGenerationStatus(strategyId: number | string): Promise<EventSource> {
const url = `${this.baseURL}/enhanced-strategies/stream/strategies?strategy_id=${strategyId}`;
return new EventSource(url);
}
async streamAutofillRefresh(userId?: number, useAI: boolean = true, aiOnly: boolean = false): Promise<EventSource> {
const params = new URLSearchParams();
if (userId) params.append('user_id', String(userId));
params.append('use_ai', String(useAI));
params.append('ai_only', String(aiOnly));
const url = `${this.baseURL}/enhanced-strategies/autofill/refresh/stream?${params.toString()}`;
return new EventSource(url);
}
async refreshAutofill(userId?: number, useAI: boolean = true, aiOnly: boolean = false): Promise<any> {
const params: any = { use_ai: useAI, ai_only: aiOnly };
if (userId) params.user_id = userId;
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/autofill/refresh`, null, { params });
return response.data;
}
// Helper method to handle SSE data
handleSSEData(eventSource: EventSource, onData: (data: any) => void, onError?: (error: any) => void, onComplete?: () => void) {
eventSource.onmessage = (event) => {

View File

@@ -26,6 +26,7 @@ export class ContentPlanningOrchestrator {
private serviceStatuses: Map<string, ServiceStatus> = new Map();
private onProgressUpdate?: (statuses: ServiceStatus[]) => void;
private onDataUpdate?: (data: Partial<DashboardData>) => void;
private latestDashboardData: DashboardData | null = null;
constructor() {
this.initializeServiceStatuses();
@@ -128,6 +129,7 @@ export class ContentPlanningOrchestrator {
}
});
this.latestDashboardData = dashboardData;
return dashboardData;
}
@@ -227,51 +229,77 @@ export class ContentPlanningOrchestrator {
message: 'Initializing AI analysis...'
});
return new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>((resolve, reject) => {
contentPlanningApi.streamAIAnalytics(
// Progress callback
(progressData) => {
this.updateServiceStatus('aiAnalytics', {
progress: progressData.progress,
message: progressData.message || 'AI analysis in progress...'
});
},
// Complete callback
(aiData) => {
this.updateServiceStatus('aiAnalytics', {
status: 'success',
progress: 100,
message: `Generated ${aiData.insights?.length || 0} insights and ${aiData.recommendations?.length || 0} recommendations`,
data: aiData
});
this.notifyDataUpdate({
aiInsights: aiData.insights || [],
aiRecommendations: aiData.recommendations || []
});
resolve({
aiInsights: aiData.insights || [],
aiRecommendations: aiData.recommendations || []
});
},
// Error callback
(error) => {
this.updateServiceStatus('aiAnalytics', {
status: 'error',
progress: 0,
message: 'AI analysis failed',
error: error.message
});
reject(error);
// New approach: stream strategic intelligence data and show status from AI generation SSE
return await new Promise<{ aiInsights: any[]; aiRecommendations: any[] }>(async (resolve) => {
// 1) Execution status stream (best-effort; ignore if no active strategy)
try {
const currentStrategyId = this.latestDashboardData?.strategies?.[0]?.id;
if (currentStrategyId) {
const statusSource = await contentPlanningApi.streamAIGenerationStatus(currentStrategyId);
statusSource.onmessage = (event: MessageEvent) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'progress') {
this.updateServiceStatus('aiAnalytics', {
status: 'loading',
progress: Math.min(99, data.progress || 20),
message: data.detail || 'AI generation in progress...'
});
}
if (data.type === 'result') {
this.updateServiceStatus('aiAnalytics', {
status: data.status === 'completed' ? 'success' : 'error',
progress: 100,
message: data.status === 'completed' ? 'AI generation completed' : 'AI generation failed'
});
statusSource.close();
}
} catch {}
};
statusSource.onerror = () => statusSource.close();
}
);
} catch {}
// 2) Data stream for insights (Strategic Intelligence)
const intelSource = await contentPlanningApi.streamStrategicIntelligence(1);
contentPlanningApi.handleSSEData(
intelSource,
(data) => {
if (data.type === 'progress') {
this.updateServiceStatus('aiAnalytics', {
status: 'loading',
progress: Math.max(20, data.progress || 40),
message: data.message || 'Analyzing strategic intelligence...'
});
} else if (data.type === 'result' && data.status === 'success') {
this.updateServiceStatus('aiAnalytics', {
status: 'success',
progress: 100,
message: 'Strategic intelligence ready',
data: data.data
});
// Map to orchestrator fields if needed
this.notifyDataUpdate({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
resolve({ aiInsights: data.data?.recommendations || [], aiRecommendations: [] });
} else if (data.type === 'error') {
this.updateServiceStatus('aiAnalytics', {
status: 'error',
progress: 0,
message: data.message || 'Failed to load strategic intelligence'
});
resolve({ aiInsights: [], aiRecommendations: [] });
}
},
() => {
resolve({ aiInsights: [], aiRecommendations: [] });
}
);
});
} catch (error: any) {
this.updateServiceStatus('aiAnalytics', {
status: 'error',
progress: 0,
message: 'AI analysis failed',
message: 'Failed to load AI analytics',
error: error.message
});
return { aiInsights: [], aiRecommendations: [] };

View File

@@ -192,7 +192,7 @@ interface EnhancedStrategyStore {
getPreviousStep: () => ProgressiveDisclosureStep | null;
// Auto-population actions
autoPopulateFromOnboarding: () => Promise<void>;
autoPopulateFromOnboarding: (forceRefresh?: boolean) => Promise<void>;
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => void;
overrideAutoPopulatedField: (fieldId: string, value: any) => void;
@@ -759,12 +759,21 @@ export const useEnhancedStrategyStore = create<EnhancedStrategyStore>((set, get)
},
// Auto-population actions
autoPopulateFromOnboarding: async () => {
autoPopulateFromOnboarding: async (forceRefresh: boolean = false) => {
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
// Optionally clear backend caches to force fresh values
if (forceRefresh) {
try {
await contentPlanningApi.clearEnhancedCache(1);
console.log('♻️ Cleared enhanced strategy cache for fresh onboarding data');
} catch (e) {
console.warn('Cache clear failed (non-blocking):', e);
}
}
// Fetch onboarding data to auto-populate fields
const response = await contentPlanningApi.getOnboardingData();
console.log('📡 Backend response:', response);