AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.

This commit is contained in:
ajaysi
2026-01-10 19:32:50 +05:30
parent 0b63ae7fc1
commit 8193cdba67
298 changed files with 45678 additions and 10952 deletions

View File

@@ -72,6 +72,7 @@ export {
Cell,
PolarGrid,
PolarAngleAxis,
PolarRadiusAxis
PolarRadiusAxis,
ReferenceLine
} from 'recharts';

View File

@@ -0,0 +1,268 @@
/**
* 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();
};

View File

@@ -0,0 +1,303 @@
/**
* Terminology Mapping Utility
*
* Maps technical terms to simple, user-friendly language for non-technical users.
* Used throughout Product Marketing and Campaign Creator components.
*/
export interface TerminologyMap {
[key: string]: {
simple: string;
description?: string;
examples?: string[];
};
}
/**
* Main terminology mapping dictionary.
* Maps technical terms to simple language.
*/
export const TERMINOLOGY: TerminologyMap = {
// Campaign Creator Terms
'Campaign Blueprint': {
simple: 'Marketing Campaign',
description: 'Your complete marketing plan with all content pieces',
},
'campaign_blueprint': {
simple: 'marketing campaign',
},
'Asset Nodes': {
simple: 'Content Pieces',
description: 'Individual images, videos, or text posts for your campaign',
},
'asset_nodes': {
simple: 'content pieces',
},
'KPI': {
simple: 'How will you measure success?',
description: 'Key Performance Indicator - how you\'ll track if your campaign is working',
examples: ['Sales increase', 'Website visits', 'Social media followers', 'Email signups'],
},
'kpi': {
simple: 'success metric',
},
'Brand DNA': {
simple: 'Your Brand Style',
description: 'Your brand\'s unique personality, colors, and voice',
},
'brand_dna': {
simple: 'brand style',
},
'Channel Pack': {
simple: 'Platform Settings',
description: 'Settings optimized for each social media platform',
},
'channel_pack': {
simple: 'platform settings',
},
'Phase Management': {
simple: 'Campaign Timeline',
description: 'The schedule for when your campaign content will be published',
},
'phase_management': {
simple: 'campaign timeline',
},
'Asset Proposals': {
simple: 'Content Ideas',
description: 'AI-generated suggestions for your campaign content',
},
'asset_proposals': {
simple: 'content ideas',
},
'Orchestration': {
simple: 'Campaign Planning',
description: 'Automatically organizing and planning your campaign',
},
'orchestration': {
simple: 'campaign planning',
},
// Product Marketing Terms
'Product Photoshoot': {
simple: 'Product Photos',
description: 'Professional photos of your product',
},
'product_photoshoot': {
simple: 'product photos',
},
'Environment': {
simple: 'Photo Setting',
description: 'Where the product photo will be taken',
examples: ['Studio', 'Lifestyle', 'Outdoor', 'Minimalist'],
},
'environment': {
simple: 'photo setting',
},
'Background Style': {
simple: 'Background',
description: 'What\'s behind your product in the photo',
examples: ['White', 'Transparent', 'Lifestyle scene', 'Branded'],
},
'background_style': {
simple: 'background',
},
'Lighting': {
simple: 'Lighting Style',
description: 'How the product is lit',
examples: ['Natural', 'Studio', 'Dramatic', 'Soft'],
},
'lighting': {
simple: 'lighting style',
},
'Resolution': {
simple: 'Image Size',
description: 'How large and detailed the image will be',
examples: ['1024x1024', '2048x2048'],
},
'resolution': {
simple: 'image size',
},
'Variations': {
simple: 'Number of Photos',
description: 'How many different photos to generate',
},
'variations': {
simple: 'number of photos',
},
'Animation Type': {
simple: 'Animation Style',
description: 'How the product will move in the video',
examples: ['Reveal', '360° Rotation', 'Demo', 'Lifestyle'],
},
'animation_type': {
simple: 'animation style',
},
'Video Type': {
simple: 'Video Style',
description: 'What kind of video to create',
examples: ['Demo', 'Storytelling', 'Feature Highlight', 'Launch'],
},
'video_type': {
simple: 'video style',
},
'Explainer Type': {
simple: 'Video Purpose',
description: 'What the video will explain',
examples: ['Product Overview', 'Feature Demo', 'Tutorial', 'Brand Message'],
},
'explainer_type': {
simple: 'video purpose',
},
// General Terms
'Asset': {
simple: 'Content',
description: 'Any image, video, or text created for marketing',
},
'asset': {
simple: 'content',
},
'Channel': {
simple: 'Platform',
description: 'Where you\'ll share your content',
examples: ['Instagram', 'Facebook', 'LinkedIn', 'TikTok'],
},
'channel': {
simple: 'platform',
},
'Template': {
simple: 'Style Preset',
description: 'A pre-designed style you can use',
},
'template': {
simple: 'style preset',
},
'Pre-flight Validation': {
simple: 'Campaign Check',
description: 'Making sure everything is ready before creating your campaign',
},
'preflight': {
simple: 'campaign check',
},
};
/**
* Get simple term for a technical term.
*
* @param technicalTerm - The technical term to translate
* @returns Simple, user-friendly term
*/
export function getSimpleTerm(technicalTerm: string): string {
const normalized = technicalTerm.trim();
// Try exact match first
if (TERMINOLOGY[normalized]) {
return TERMINOLOGY[normalized].simple;
}
// Try case-insensitive match
const lowerKey = Object.keys(TERMINOLOGY).find(
key => key.toLowerCase() === normalized.toLowerCase()
);
if (lowerKey) {
return TERMINOLOGY[lowerKey].simple;
}
// Return original if no match found
return technicalTerm;
}
/**
* Get description for a technical term.
*
* @param technicalTerm - The technical term
* @returns Description or undefined
*/
export function getTermDescription(technicalTerm: string): string | undefined {
const normalized = technicalTerm.trim();
if (TERMINOLOGY[normalized]?.description) {
return TERMINOLOGY[normalized].description;
}
const lowerKey = Object.keys(TERMINOLOGY).find(
key => key.toLowerCase() === normalized.toLowerCase()
);
if (lowerKey && TERMINOLOGY[lowerKey].description) {
return TERMINOLOGY[lowerKey].description;
}
return undefined;
}
/**
* Get examples for a technical term.
*
* @param technicalTerm - The technical term
* @returns Array of examples or undefined
*/
export function getTermExamples(technicalTerm: string): string[] | undefined {
const normalized = technicalTerm.trim();
if (TERMINOLOGY[normalized]?.examples) {
return TERMINOLOGY[normalized].examples;
}
const lowerKey = Object.keys(TERMINOLOGY).find(
key => key.toLowerCase() === normalized.toLowerCase()
);
if (lowerKey && TERMINOLOGY[lowerKey].examples) {
return TERMINOLOGY[lowerKey].examples;
}
return undefined;
}
/**
* Replace technical terms in text with simple terms.
*
* @param text - Text containing technical terms
* @returns Text with simple terms
*/
export function simplifyText(text: string): string {
let simplified = text;
// Replace all known technical terms
Object.keys(TERMINOLOGY).forEach(technicalTerm => {
const simpleTerm = TERMINOLOGY[technicalTerm].simple;
// Case-insensitive replacement
const regex = new RegExp(technicalTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
simplified = simplified.replace(regex, simpleTerm);
});
return simplified;
}
/**
* Helper function to get tooltip text for a field.
* Combines description and examples if available.
*
* @param technicalTerm - The technical term
* @returns Tooltip text
*/
export function getTooltipText(technicalTerm: string): string {
const description = getTermDescription(technicalTerm);
const examples = getTermExamples(technicalTerm);
let tooltip = '';
if (description) {
tooltip = description;
}
if (examples && examples.length > 0) {
if (tooltip) {
tooltip += '\n\nExamples: ';
} else {
tooltip = 'Examples: ';
}
tooltip += examples.join(', ');
}
return tooltip || getSimpleTerm(technicalTerm);
}

View File

@@ -0,0 +1,30 @@
import { Step } from 'react-joyride';
export const campaignCreatorSteps: Step[] = [
{
target: 'body',
content: 'Welcome to the Campaign Creator! Lets cover the essentials in under a minute.',
disableBeacon: true,
placement: 'center',
},
{
target: '[data-tour="cc-recommendations"]',
content: 'Start with AI recommendations tailored to your industry, style, and platforms.',
placement: 'bottom',
},
{
target: '[data-tour="cc-journeys"]',
content: 'Pick a journey: launch a campaign, audit assets, or go straight to a studio.',
placement: 'top',
},
{
target: '[data-tour="quick-actions"]',
content: 'Quick actions for creating campaigns or auditing existing assets.',
placement: 'top',
},
{
target: '[data-tour="active-campaigns"]',
content: 'Monitor active campaigns and jump back into proposals or asset generation.',
placement: 'top',
},
];

View File

@@ -0,0 +1,30 @@
import { Step } from 'react-joyride';
export const productMarketingSteps: Step[] = [
{
target: 'body',
content: 'Welcome! This short tour shows how to generate product assets fast.',
disableBeacon: true,
placement: 'center',
},
{
target: '[data-tour="pm-recommendations"]',
content: 'Personalized picks based on your onboarding—templates, platforms, and quick starts.',
placement: 'bottom',
},
{
target: '[data-tour="pm-product-grid"]',
content: 'Jump into product studios: animations, videos, and avatars tailored to your brand.',
placement: 'top',
},
{
target: '[data-tour="quick-actions"]',
content: 'Quick actions to create a campaign or audit existing assets without extra steps.',
placement: 'top',
},
{
target: '[data-tour="active-campaigns"]',
content: 'Track active campaigns and reopen proposals or assets from here.',
placement: 'top',
},
];