ALwrity version 0.5.4
This commit is contained in:
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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: [] };
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user