Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts
This commit is contained in:
@@ -2,8 +2,10 @@
|
||||
export { useDashboardStore } from './dashboardStore';
|
||||
export { useSEODashboardStore } from './seoDashboardStore';
|
||||
export { useSharedDashboardStore } from './sharedDashboardStore';
|
||||
export { useSemanticDashboardStore } from './semanticDashboardStore';
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { DashboardStore } from './dashboardStore';
|
||||
export type { SEODashboardStore } from './seoDashboardStore';
|
||||
export type { SharedDashboardState } from './sharedDashboardStore';
|
||||
export type { SharedDashboardState } from './sharedDashboardStore';
|
||||
export type { SemanticDashboardStore } from './semanticDashboardStore';
|
||||
188
frontend/src/stores/semanticDashboardStore.ts
Normal file
188
frontend/src/stores/semanticDashboardStore.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import {
|
||||
SemanticHealthMetric,
|
||||
CompetitorSemanticSnapshot,
|
||||
ContentSemanticInsight,
|
||||
SemanticDashboardData
|
||||
} from '../api/semanticDashboard';
|
||||
import { semanticDashboardAPI } from '../api/semanticDashboard';
|
||||
|
||||
export interface SemanticDashboardStore {
|
||||
// State
|
||||
semanticHealth: SemanticHealthMetric | null;
|
||||
competitorSnapshots: CompetitorSemanticSnapshot[];
|
||||
contentInsights: ContentSemanticInsight[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: string | null;
|
||||
monitoringStatus: 'active' | 'inactive';
|
||||
|
||||
// Actions
|
||||
setSemanticHealth: (health: SemanticHealthMetric | null) => void;
|
||||
setCompetitorSnapshots: (snapshots: CompetitorSemanticSnapshot[]) => void;
|
||||
setContentInsights: (insights: ContentSemanticInsight[]) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
setLastUpdated: (timestamp: string | null) => void;
|
||||
setMonitoringStatus: (status: 'active' | 'inactive') => void;
|
||||
|
||||
// Async actions
|
||||
fetchSemanticHealth: () => Promise<void>;
|
||||
fetchCompetitorSnapshots: () => Promise<void>;
|
||||
fetchContentInsights: () => Promise<void>;
|
||||
fetchAllSemanticData: () => Promise<void>;
|
||||
refreshSemanticAnalysis: () => Promise<void>;
|
||||
|
||||
// Utility functions
|
||||
getHealthStatusColor: (status: string) => string;
|
||||
getInsightTypeColor: (type: string) => string;
|
||||
getConfidenceColor: (score: number) => string;
|
||||
}
|
||||
|
||||
export const useSemanticDashboardStore = create<SemanticDashboardStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
semanticHealth: null,
|
||||
competitorSnapshots: [],
|
||||
contentInsights: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
lastUpdated: null,
|
||||
monitoringStatus: 'inactive',
|
||||
|
||||
// Actions
|
||||
setSemanticHealth: (health) => set({ semanticHealth: health }),
|
||||
setCompetitorSnapshots: (snapshots) => set({ competitorSnapshots: snapshots }),
|
||||
setContentInsights: (insights) => set({ contentInsights: insights }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setLastUpdated: (timestamp) => set({ lastUpdated: timestamp }),
|
||||
setMonitoringStatus: (status) => set({ monitoringStatus: status }),
|
||||
|
||||
// Fetch semantic health metrics
|
||||
fetchSemanticHealth: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
const health = await semanticDashboardAPI.getSemanticHealth();
|
||||
set({
|
||||
semanticHealth: health,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
monitoringStatus: 'active'
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch semantic health';
|
||||
set({ error: errorMessage });
|
||||
console.error('Error fetching semantic health:', error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch competitor snapshots
|
||||
fetchCompetitorSnapshots: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
const snapshots = await semanticDashboardAPI.getCompetitorSnapshots();
|
||||
set({ competitorSnapshots: snapshots, lastUpdated: new Date().toISOString() });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch competitor snapshots';
|
||||
set({ error: errorMessage });
|
||||
console.error('Error fetching competitor snapshots:', error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch content insights
|
||||
fetchContentInsights: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
const insights = await semanticDashboardAPI.getContentInsights();
|
||||
set({ contentInsights: insights, lastUpdated: new Date().toISOString() });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch content insights';
|
||||
set({ error: errorMessage });
|
||||
console.error('Error fetching content insights:', error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch all semantic data
|
||||
fetchAllSemanticData: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
|
||||
// Fetch all data in parallel
|
||||
const [health, snapshots, insights] = await Promise.all([
|
||||
semanticDashboardAPI.getSemanticHealth(),
|
||||
semanticDashboardAPI.getCompetitorSnapshots(),
|
||||
semanticDashboardAPI.getContentInsights()
|
||||
]);
|
||||
|
||||
set({
|
||||
semanticHealth: health,
|
||||
competitorSnapshots: snapshots,
|
||||
contentInsights: insights,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
monitoringStatus: 'active'
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to fetch semantic data';
|
||||
set({ error: errorMessage });
|
||||
console.error('Error fetching all semantic data:', error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Refresh semantic analysis
|
||||
refreshSemanticAnalysis: async () => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
await semanticDashboardAPI.refreshSemanticAnalysis();
|
||||
|
||||
// Refetch all data after refresh
|
||||
await get().fetchAllSemanticData();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to refresh semantic analysis';
|
||||
set({ error: errorMessage });
|
||||
console.error('Error refreshing semantic analysis:', error);
|
||||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Utility functions
|
||||
getHealthStatusColor: (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return '#4CAF50';
|
||||
case 'warning': return '#FF9800';
|
||||
case 'critical': return '#F44336';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
},
|
||||
|
||||
getInsightTypeColor: (type: string) => {
|
||||
switch (type) {
|
||||
case 'opportunity': return '#4CAF50';
|
||||
case 'trend': return '#2196F3';
|
||||
case 'threat': return '#F44336';
|
||||
case 'gap': return '#FF9800';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
},
|
||||
|
||||
getConfidenceColor: (score: number) => {
|
||||
if (score >= 0.8) return '#4CAF50';
|
||||
if (score >= 0.6) return '#FF9800';
|
||||
return '#F44336';
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'semantic-dashboard-store',
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -6,9 +6,55 @@ import {
|
||||
WorkflowProgress,
|
||||
UserWorkflowPreferences,
|
||||
NavigationState,
|
||||
WorkflowStatus,
|
||||
WorkflowError
|
||||
} from '../types/workflow';
|
||||
import { taskWorkflowOrchestrator } from '../services/TaskWorkflowOrchestrator';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
const isServerWorkflowId = (workflowId: string) => workflowId.startsWith('daily-');
|
||||
|
||||
const computeProgressAndNavigation = (workflow: DailyWorkflow): { progress: WorkflowProgress; navigation: NavigationState } => {
|
||||
const tasks = Array.isArray(workflow.tasks) ? workflow.tasks : [];
|
||||
const totalTasks = tasks.length;
|
||||
const completedTasks = tasks.filter(t => t.status === 'completed' || t.status === 'skipped').length;
|
||||
const completionPercentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
|
||||
|
||||
const currentIndex = (() => {
|
||||
if (typeof workflow.currentTaskIndex === 'number' && workflow.currentTaskIndex >= 0 && workflow.currentTaskIndex < totalTasks) {
|
||||
return workflow.currentTaskIndex;
|
||||
}
|
||||
const idx = tasks.findIndex(t => t.status !== 'completed' && t.status !== 'skipped');
|
||||
return idx >= 0 ? idx : Math.max(0, totalTasks - 1);
|
||||
})();
|
||||
|
||||
const currentTask = tasks[currentIndex] || null;
|
||||
const nextTask = tasks.slice(currentIndex + 1).find(t => t.status !== 'completed' && t.status !== 'skipped') || null;
|
||||
const previousTask = currentIndex > 0 ? tasks[currentIndex - 1] : null;
|
||||
|
||||
const estimatedTimeRemaining = tasks
|
||||
.filter(t => t.status !== 'completed' && t.status !== 'skipped')
|
||||
.reduce((sum, t) => sum + (t.estimatedTime || 0), 0);
|
||||
|
||||
return {
|
||||
progress: {
|
||||
completedTasks,
|
||||
totalTasks,
|
||||
completionPercentage,
|
||||
currentTask: currentTask || undefined,
|
||||
nextTask: nextTask || undefined,
|
||||
estimatedTimeRemaining,
|
||||
actualTimeSpent: workflow.actualTimeSpent || 0,
|
||||
},
|
||||
navigation: {
|
||||
currentTask,
|
||||
previousTask,
|
||||
nextTask,
|
||||
canGoBack: currentIndex > 0,
|
||||
canGoForward: Boolean(nextTask),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
interface WorkflowState {
|
||||
// Current workflow state
|
||||
@@ -68,16 +114,25 @@ export const useWorkflowStore = create<WorkflowState>()(
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
try {
|
||||
const resp = await apiClient.get('/api/today-workflow', { params: date ? { date } : {} });
|
||||
const serverWorkflow = resp?.data?.data?.workflow as DailyWorkflow | undefined;
|
||||
if (serverWorkflow && Array.isArray(serverWorkflow.tasks)) {
|
||||
const derived = computeProgressAndNavigation(serverWorkflow);
|
||||
set({
|
||||
currentWorkflow: serverWorkflow,
|
||||
workflowProgress: derived.progress,
|
||||
navigationState: derived.navigation,
|
||||
isLoading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const workflow = await taskWorkflowOrchestrator.generateDailyWorkflow(userId, date);
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(workflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(workflow.id);
|
||||
|
||||
set({
|
||||
currentWorkflow: workflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
set({ currentWorkflow: workflow, workflowProgress: progress, navigationState: navigation, isLoading: false });
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
@@ -92,16 +147,24 @@ export const useWorkflowStore = create<WorkflowState>()(
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const current = get().currentWorkflow;
|
||||
if (current && current.id === workflowId && isServerWorkflowId(workflowId)) {
|
||||
const tasks = current.tasks.map((t, idx) => {
|
||||
if (idx === current.currentTaskIndex && t.status === 'pending') {
|
||||
return { ...t, status: 'in_progress' as const, startedAt: new Date() };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
const updated: DailyWorkflow = { ...current, workflowStatus: 'in_progress', startedAt: new Date(), tasks };
|
||||
const derived = computeProgressAndNavigation(updated);
|
||||
set({ currentWorkflow: updated, workflowProgress: derived.progress, navigationState: derived.navigation, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const workflow = await taskWorkflowOrchestrator.startWorkflow(workflowId);
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(workflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(workflow.id);
|
||||
|
||||
set({
|
||||
currentWorkflow: workflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
set({ currentWorkflow: workflow, workflowProgress: progress, navigationState: navigation, isLoading: false });
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
@@ -177,25 +240,33 @@ export const useWorkflowStore = create<WorkflowState>()(
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const progress = await taskWorkflowOrchestrator.completeTask(
|
||||
currentWorkflow.id,
|
||||
taskId,
|
||||
completionData
|
||||
);
|
||||
if (isServerWorkflowId(currentWorkflow.id)) {
|
||||
await apiClient.post(`/api/today-workflow/tasks/${taskId}/status`, {
|
||||
status: 'completed',
|
||||
completion_notes: completionData?.userNotes
|
||||
});
|
||||
|
||||
const tasks = currentWorkflow.tasks.map(t => (t.id === taskId ? { ...t, status: 'completed' as const, completedAt: new Date() } : t));
|
||||
const completedTasks = tasks.filter(t => t.status === 'completed' || t.status === 'skipped').length;
|
||||
const totalTasks = tasks.length;
|
||||
const workflowStatus: WorkflowStatus = totalTasks > 0 && completedTasks === totalTasks ? 'completed' : 'in_progress';
|
||||
const updated: DailyWorkflow = {
|
||||
...currentWorkflow,
|
||||
tasks,
|
||||
completedTasks,
|
||||
totalTasks,
|
||||
workflowStatus,
|
||||
completedAt: workflowStatus === 'completed' ? new Date() : currentWorkflow.completedAt,
|
||||
};
|
||||
const derived = computeProgressAndNavigation(updated);
|
||||
set({ currentWorkflow: updated, workflowProgress: derived.progress, navigationState: derived.navigation, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = await taskWorkflowOrchestrator.completeTask(currentWorkflow.id, taskId, completionData);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
// Update current workflow
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(
|
||||
currentWorkflow.userId,
|
||||
currentWorkflow.date
|
||||
);
|
||||
|
||||
set({
|
||||
currentWorkflow: updatedWorkflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(currentWorkflow.userId, currentWorkflow.date);
|
||||
set({ currentWorkflow: updatedWorkflow, workflowProgress: progress, navigationState: navigation, isLoading: false });
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
@@ -213,24 +284,22 @@ export const useWorkflowStore = create<WorkflowState>()(
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const progress = await taskWorkflowOrchestrator.skipTask(
|
||||
currentWorkflow.id,
|
||||
taskId
|
||||
);
|
||||
if (isServerWorkflowId(currentWorkflow.id)) {
|
||||
await apiClient.post(`/api/today-workflow/tasks/${taskId}/status`, { status: 'skipped' });
|
||||
const tasks = currentWorkflow.tasks.map(t => (t.id === taskId ? { ...t, status: 'skipped' as const, completedAt: new Date() } : t));
|
||||
const completedTasks = tasks.filter(t => t.status === 'completed' || t.status === 'skipped').length;
|
||||
const totalTasks = tasks.length;
|
||||
const workflowStatus: WorkflowStatus = totalTasks > 0 && completedTasks === totalTasks ? 'completed' : currentWorkflow.workflowStatus;
|
||||
const updated: DailyWorkflow = { ...currentWorkflow, tasks, completedTasks, totalTasks, workflowStatus };
|
||||
const derived = computeProgressAndNavigation(updated);
|
||||
set({ currentWorkflow: updated, workflowProgress: derived.progress, navigationState: derived.navigation, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = await taskWorkflowOrchestrator.skipTask(currentWorkflow.id, taskId);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
// Update current workflow
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(
|
||||
currentWorkflow.userId,
|
||||
currentWorkflow.date
|
||||
);
|
||||
|
||||
set({
|
||||
currentWorkflow: updatedWorkflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(currentWorkflow.userId, currentWorkflow.date);
|
||||
set({ currentWorkflow: updatedWorkflow, workflowProgress: progress, navigationState: navigation, isLoading: false });
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
@@ -248,22 +317,23 @@ export const useWorkflowStore = create<WorkflowState>()(
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
if (isServerWorkflowId(currentWorkflow.id)) {
|
||||
const tasks = currentWorkflow.tasks;
|
||||
const nextIndex = tasks.findIndex((t, idx) => idx > currentWorkflow.currentTaskIndex && t.status !== 'completed' && t.status !== 'skipped');
|
||||
const updated: DailyWorkflow = {
|
||||
...currentWorkflow,
|
||||
currentTaskIndex: nextIndex >= 0 ? nextIndex : currentWorkflow.currentTaskIndex,
|
||||
};
|
||||
const derived = computeProgressAndNavigation(updated);
|
||||
set({ currentWorkflow: updated, workflowProgress: derived.progress, navigationState: derived.navigation, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
await taskWorkflowOrchestrator.moveToNextTask(currentWorkflow.id);
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(currentWorkflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
// Update current workflow
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(
|
||||
currentWorkflow.userId,
|
||||
currentWorkflow.date
|
||||
);
|
||||
|
||||
set({
|
||||
currentWorkflow: updatedWorkflow,
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation,
|
||||
isLoading: false
|
||||
});
|
||||
const updatedWorkflow = taskWorkflowOrchestrator.getWorkflow(currentWorkflow.userId, currentWorkflow.date);
|
||||
set({ currentWorkflow: updatedWorkflow, workflowProgress: progress, navigationState: navigation, isLoading: false });
|
||||
} catch (error) {
|
||||
const workflowError = error as WorkflowError;
|
||||
set({
|
||||
@@ -321,13 +391,15 @@ export const useWorkflowStore = create<WorkflowState>()(
|
||||
if (!currentWorkflow) return;
|
||||
|
||||
try {
|
||||
if (isServerWorkflowId(currentWorkflow.id)) {
|
||||
const derived = computeProgressAndNavigation(currentWorkflow);
|
||||
set({ workflowProgress: derived.progress, navigationState: derived.navigation });
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = taskWorkflowOrchestrator.getWorkflowProgress(currentWorkflow.id);
|
||||
const navigation = taskWorkflowOrchestrator.getNavigationState(currentWorkflow.id);
|
||||
|
||||
set({
|
||||
workflowProgress: progress,
|
||||
navigationState: navigation
|
||||
});
|
||||
set({ workflowProgress: progress, navigationState: navigation });
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh workflow progress:', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user