Files
ALwrity/frontend/src/utils/researchDraftManager.ts

269 lines
10 KiB
TypeScript

/**
* Research Draft Manager
*
* Handles saving and restoring research drafts at each stage of the research process.
* Drafts are saved to localStorage for quick access and to the database for persistence.
*/
import { WizardState } from '../components/Research/types/research.types';
import { AnalyzeIntentResponse, ResearchIntent, IntentDrivenResearchResponse } from '../components/Research/types/intent.types';
import { BlogResearchResponse } from '../services/blogWriterApi';
import { intentResearchApi } from '../api/intentResearchApi';
const DRAFT_STORAGE_KEY = 'alwrity_research_draft';
const DRAFT_ID_KEY = 'alwrity_research_draft_id';
export interface ResearchDraft {
id?: string; // Database draft ID (database primary key)
project_id?: string; // Project UUID (for lookups)
keywords: string[];
industry: string;
target_audience: string;
research_mode: string;
config: any;
current_step: number;
intent_analysis?: AnalyzeIntentResponse | null;
confirmed_intent?: ResearchIntent | null;
intent_result?: IntentDrivenResearchResponse | null;
legacy_result?: BlogResearchResponse | null;
trends_config?: any;
created_at: string;
updated_at: string;
is_complete: boolean;
}
/**
* Save draft to localStorage
*/
export const saveDraftToStorage = (draft: Partial<ResearchDraft>): void => {
try {
const existingDraft = getDraftFromStorage();
const now = new Date().toISOString();
const mergedDraft: Partial<ResearchDraft> = {
...existingDraft,
...draft,
updated_at: now,
// Preserve project_id if it exists
project_id: draft.project_id || existingDraft?.project_id,
// Ensure required fields have defaults
keywords: draft.keywords || existingDraft?.keywords || [],
industry: draft.industry || existingDraft?.industry || 'General',
target_audience: draft.target_audience || existingDraft?.target_audience || 'General',
research_mode: draft.research_mode || existingDraft?.research_mode || 'comprehensive',
config: draft.config || existingDraft?.config || {},
current_step: draft.current_step || existingDraft?.current_step || 1,
created_at: draft.created_at || existingDraft?.created_at || now,
is_complete: draft.is_complete ?? existingDraft?.is_complete ?? false,
};
localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(mergedDraft));
console.log('[ResearchDraftManager] ✅ Draft saved to localStorage:', {
step: mergedDraft.current_step,
hasKeywords: (mergedDraft.keywords?.length || 0) > 0,
hasIntentAnalysis: !!mergedDraft.intent_analysis,
hasConfirmedIntent: !!mergedDraft.confirmed_intent,
hasResults: !!mergedDraft.intent_result || !!mergedDraft.legacy_result,
});
} catch (error) {
console.error('[ResearchDraftManager] ❌ Failed to save draft to localStorage:', error);
}
};
/**
* Get draft from localStorage
*/
export const getDraftFromStorage = (): Partial<ResearchDraft> | null => {
try {
const draftJson = localStorage.getItem(DRAFT_STORAGE_KEY);
if (!draftJson) return null;
const draft = JSON.parse(draftJson);
return draft;
} catch (error) {
console.error('[ResearchDraftManager] ❌ Failed to load draft from localStorage:', error);
return null;
}
};
/**
* Clear draft from localStorage
*/
export const clearDraftFromStorage = (): void => {
localStorage.removeItem(DRAFT_STORAGE_KEY);
localStorage.removeItem(DRAFT_ID_KEY);
console.log('[ResearchDraftManager] ✅ Draft cleared from localStorage');
};
/**
* Create draft from wizard state
*/
export const createDraftFromState = (
state: WizardState,
options?: {
intentAnalysis?: AnalyzeIntentResponse | null;
confirmedIntent?: ResearchIntent | null;
intentResult?: IntentDrivenResearchResponse | null;
legacyResult?: BlogResearchResponse | null;
trendsConfig?: any;
}
): Partial<ResearchDraft> => {
const now = new Date().toISOString();
const existingDraft = getDraftFromStorage();
return {
id: existingDraft?.id,
project_id: existingDraft?.project_id,
keywords: state.keywords || [],
industry: state.industry || 'General',
target_audience: state.targetAudience || 'General',
research_mode: state.researchMode || 'comprehensive',
config: state.config || {},
current_step: state.currentStep || 1,
intent_analysis: options?.intentAnalysis || existingDraft?.intent_analysis,
confirmed_intent: options?.confirmedIntent || existingDraft?.confirmed_intent,
intent_result: options?.intentResult || existingDraft?.intent_result,
legacy_result: options?.legacyResult || existingDraft?.legacy_result,
trends_config: options?.trendsConfig || existingDraft?.trends_config,
created_at: existingDraft?.created_at || now,
updated_at: now,
is_complete: !!(options?.intentResult || options?.legacyResult),
};
};
/**
* Save draft to database (Asset Library)
* This creates/updates a draft in the database
*/
export const saveDraftToDatabase = async (
draft: Partial<ResearchDraft>
): Promise<{ success: boolean; draft_id?: string; message: string }> => {
try {
const keywords = draft.keywords || [];
const industry = draft.industry || 'General';
const targetAudience = draft.target_audience || 'General';
const currentStep = draft.current_step || 1;
// Generate title from keywords
const title = keywords.length > 0
? `Draft: ${keywords.slice(0, 3).join(', ')}`
: 'Research Draft';
// Generate description
const description = draft.is_complete
? `Completed research on ${keywords.join(', ')}`
: `Research draft - Step ${currentStep} of 3. ` +
`Industry: ${industry}, Target Audience: ${targetAudience}`;
// Convert draft to wizard state format for API
const wizardState: WizardState = {
currentStep,
keywords,
industry,
targetAudience,
researchMode: (draft.research_mode as any) || 'comprehensive',
config: draft.config || {},
results: draft.legacy_result || null, // Only use legacy_result for WizardState.results
};
// Save using existing API (pass project_id if we have one for updates)
const existingProjectId = localStorage.getItem(DRAFT_ID_KEY) || draft.project_id;
const response = await intentResearchApi.saveResearchProject(wizardState, {
intentAnalysis: draft.intent_analysis || undefined,
confirmedIntent: draft.confirmed_intent || undefined,
intentResult: draft.intent_result || undefined,
legacyResult: draft.legacy_result || undefined,
title,
description,
projectId: existingProjectId || undefined, // Pass existing project_id (UUID) for updates
});
if (response.success && response.project_id) {
// Store project_id (UUID) for future updates - this is what we use for lookups
localStorage.setItem(DRAFT_ID_KEY, response.project_id);
console.log('[ResearchDraftManager] ✅ Draft saved to database:', {
project_id: response.project_id,
db_id: response.asset_id
});
}
return {
success: response.success,
draft_id: response.project_id || (response.asset_id ? String(response.asset_id) : undefined),
message: response.message || (response.success ? 'Draft saved successfully' : 'Failed to save draft'),
};
} catch (error) {
console.error('[ResearchDraftManager] ❌ Failed to save draft to database:', error);
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to save draft',
};
}
};
/**
* Auto-save draft (saves to both localStorage and database)
*/
export const autoSaveDraft = async (
state: WizardState,
options?: {
intentAnalysis?: AnalyzeIntentResponse | null;
confirmedIntent?: ResearchIntent | null;
intentResult?: IntentDrivenResearchResponse | null;
legacyResult?: BlogResearchResponse | null;
trendsConfig?: any;
}
): Promise<void> => {
// Always save to localStorage immediately
const draft = createDraftFromState(state, options);
saveDraftToStorage(draft);
// Save to database if we have meaningful content
// Only save to DB if:
// 1. Intent analysis is complete (user clicked "intent and options"), OR
// 2. Research is complete
// NOTE: We do NOT save to DB just for keywords - only after user clicks "intent and options"
const shouldSaveToDB =
!!options?.intentAnalysis ||
!!options?.intentResult ||
!!options?.legacyResult;
if (shouldSaveToDB) {
// For intent analysis completion, save immediately (no debounce)
// For other saves (research completion), debounce to avoid excessive saves
const isIntentAnalysisSave = !!options?.intentAnalysis && !options?.intentResult && !options?.legacyResult;
if (isIntentAnalysisSave) {
// Immediate save for intent analysis (user clicked "Intent and Options")
try {
await saveDraftToDatabase(draft);
console.log('[ResearchDraftManager] ✅ Draft saved immediately after intent analysis');
} catch (error) {
// Don't block UI if database save fails - localStorage is already saved
console.warn('[ResearchDraftManager] ⚠️ Database save failed, but localStorage is saved:', error);
}
} else {
// Debounce database saves for other operations - only save every 5 seconds max
const lastSaveTime = localStorage.getItem('alwrity_last_draft_db_save');
const now = Date.now();
const shouldSaveNow = !lastSaveTime || (now - parseInt(lastSaveTime)) > 5000;
if (shouldSaveNow) {
try {
await saveDraftToDatabase(draft);
localStorage.setItem('alwrity_last_draft_db_save', String(now));
} catch (error) {
// Don't block UI if database save fails - localStorage is already saved
console.warn('[ResearchDraftManager] ⚠️ Database save failed, but localStorage is saved:', error);
}
}
}
}
};
/**
* Restore draft from storage
*/
export const restoreDraft = (): Partial<ResearchDraft> | null => {
return getDraftFromStorage();
};