AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
@@ -72,6 +72,7 @@ export {
|
||||
Cell,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
PolarRadiusAxis
|
||||
PolarRadiusAxis,
|
||||
ReferenceLine
|
||||
} from 'recharts';
|
||||
|
||||
|
||||
268
frontend/src/utils/researchDraftManager.ts
Normal file
268
frontend/src/utils/researchDraftManager.ts
Normal 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();
|
||||
};
|
||||
303
frontend/src/utils/terminology.ts
Normal file
303
frontend/src/utils/terminology.ts
Normal 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);
|
||||
}
|
||||
30
frontend/src/utils/walkthroughs/campaignCreatorSteps.ts
Normal file
30
frontend/src/utils/walkthroughs/campaignCreatorSteps.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Step } from 'react-joyride';
|
||||
|
||||
export const campaignCreatorSteps: Step[] = [
|
||||
{
|
||||
target: 'body',
|
||||
content: 'Welcome to the Campaign Creator! Let’s 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',
|
||||
},
|
||||
];
|
||||
30
frontend/src/utils/walkthroughs/productMarketingSteps.ts
Normal file
30
frontend/src/utils/walkthroughs/productMarketingSteps.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user