AI Analysis and Content Strategy fixes. Enhanced Strategy Routes refactoring.
This commit is contained in:
502
frontend/src/hooks/useCampaignCreator.ts
Normal file
502
frontend/src/hooks/useCampaignCreator.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { aiApiClient } from '../api/client';
|
||||
|
||||
export interface CampaignCreateRequest {
|
||||
campaign_name: string;
|
||||
goal: string;
|
||||
kpi?: string;
|
||||
channels: string[];
|
||||
product_context?: {
|
||||
product_description?: string;
|
||||
product_name?: string;
|
||||
marketing_goal?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CampaignBlueprint {
|
||||
campaign_id: string;
|
||||
campaign_name: string;
|
||||
goal: string;
|
||||
kpi?: string;
|
||||
phases: Array<{
|
||||
name: string;
|
||||
duration_days: number;
|
||||
purpose: string;
|
||||
}>;
|
||||
asset_nodes: Array<{
|
||||
asset_id: string;
|
||||
asset_type: string;
|
||||
channel: string;
|
||||
status: string;
|
||||
}>;
|
||||
channels: string[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AssetProposal {
|
||||
asset_id: string;
|
||||
asset_type: string;
|
||||
channel: string;
|
||||
campaign_id?: string;
|
||||
proposed_prompt?: string;
|
||||
recommended_template?: string;
|
||||
recommended_provider?: string;
|
||||
cost_estimate: number;
|
||||
concept_summary: string;
|
||||
video_subtype?: string;
|
||||
video_type?: string;
|
||||
animation_type?: string;
|
||||
duration?: number;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface AssetProposalsResponse {
|
||||
proposals: Record<string, AssetProposal>;
|
||||
total_assets: number;
|
||||
}
|
||||
|
||||
export interface BrandDNATokens {
|
||||
writing_style: {
|
||||
tone: string;
|
||||
voice: string;
|
||||
complexity: string;
|
||||
engagement_level: string;
|
||||
};
|
||||
target_audience: {
|
||||
demographics: string[];
|
||||
industry_focus: string;
|
||||
expertise_level: string;
|
||||
};
|
||||
visual_identity: {
|
||||
color_palette?: string[];
|
||||
brand_values?: string[];
|
||||
positioning?: string;
|
||||
style_guidelines?: any;
|
||||
};
|
||||
persona: {
|
||||
persona_name?: string;
|
||||
archetype?: string;
|
||||
core_belief?: string;
|
||||
linguistic_fingerprint?: any;
|
||||
platform_personas?: any;
|
||||
};
|
||||
competitive_positioning: {
|
||||
differentiators: string[];
|
||||
unique_value_props: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChannelPack {
|
||||
channel: string;
|
||||
platform: string;
|
||||
asset_type: string;
|
||||
templates: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
dimensions: string;
|
||||
aspect_ratio: string;
|
||||
recommended_provider: string;
|
||||
quality: string;
|
||||
}>;
|
||||
formats: Array<{
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: string;
|
||||
safe_zone?: any;
|
||||
}>;
|
||||
copy_framework: Record<string, any>;
|
||||
optimization_tips: string[];
|
||||
}
|
||||
|
||||
export interface AssetAuditResult {
|
||||
asset_info: {
|
||||
width: number;
|
||||
height: number;
|
||||
format: string;
|
||||
mode: string;
|
||||
quality_score: number;
|
||||
};
|
||||
recommendations: Array<{
|
||||
operation: string;
|
||||
priority: string;
|
||||
reason: string;
|
||||
suggested_mode?: string;
|
||||
suggested_format?: string;
|
||||
suggested_operations?: string[];
|
||||
}>;
|
||||
status: 'usable' | 'needs_enhancement' | 'error';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PreflightValidationResult {
|
||||
can_proceed: boolean;
|
||||
message?: string;
|
||||
error_details?: Record<string, any>;
|
||||
summary: {
|
||||
total_assets: number;
|
||||
image_count: number;
|
||||
text_count: number;
|
||||
estimated_cost: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useCampaignCreator = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Campaign Blueprint
|
||||
const [blueprint, setBlueprint] = useState<CampaignBlueprint | null>(null);
|
||||
const [isCreatingBlueprint, setIsCreatingBlueprint] = useState(false);
|
||||
|
||||
// Asset Proposals
|
||||
const [proposals, setProposals] = useState<AssetProposalsResponse | null>(null);
|
||||
const [isGeneratingProposals, setIsGeneratingProposals] = useState(false);
|
||||
|
||||
// Brand DNA
|
||||
const [brandDNA, setBrandDNA] = useState<BrandDNATokens | null>(null);
|
||||
const [isLoadingBrandDNA, setIsLoadingBrandDNA] = useState(false);
|
||||
|
||||
// Channel Packs
|
||||
const [channelPack, setChannelPack] = useState<ChannelPack | null>(null);
|
||||
const [isLoadingChannelPack, setIsLoadingChannelPack] = useState(false);
|
||||
|
||||
// Asset Audit
|
||||
const [auditResult, setAuditResult] = useState<AssetAuditResult | null>(null);
|
||||
const [isAuditing, setIsAuditing] = useState(false);
|
||||
|
||||
// Asset Generation
|
||||
const [isGeneratingAsset, setIsGeneratingAsset] = useState(false);
|
||||
const [generatedAsset, setGeneratedAsset] = useState<any>(null);
|
||||
|
||||
// Pre-flight Validation
|
||||
const [preflightResult, setPreflightResult] = useState<PreflightValidationResult | null>(null);
|
||||
const [isValidatingPreflight, setIsValidatingPreflight] = useState(false);
|
||||
|
||||
// Campaign listing
|
||||
const [campaigns, setCampaigns] = useState<CampaignBlueprint[]>([]);
|
||||
const [isLoadingCampaigns, setIsLoadingCampaigns] = useState(false);
|
||||
|
||||
const createCampaignBlueprint = useCallback(
|
||||
async (request: CampaignCreateRequest): Promise<CampaignBlueprint> => {
|
||||
setIsCreatingBlueprint(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<CampaignBlueprint>(
|
||||
'/api/campaign-creator/campaigns/create-blueprint',
|
||||
request
|
||||
);
|
||||
setBlueprint(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create campaign blueprint';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsCreatingBlueprint(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const generateAssetProposals = useCallback(
|
||||
async (campaignId: string, productContext?: any): Promise<AssetProposalsResponse> => {
|
||||
setIsGeneratingProposals(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<AssetProposalsResponse>(
|
||||
`/api/campaign-creator/campaigns/${campaignId}/generate-proposals`,
|
||||
{
|
||||
campaign_id: campaignId,
|
||||
product_context: productContext,
|
||||
}
|
||||
);
|
||||
setProposals(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate asset proposals';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingProposals(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const generateAsset = useCallback(
|
||||
async (assetProposal: AssetProposal, productContext?: any): Promise<any> => {
|
||||
setIsGeneratingAsset(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post('/api/campaign-creator/assets/generate', {
|
||||
asset_proposal: assetProposal,
|
||||
product_context: productContext,
|
||||
});
|
||||
setGeneratedAsset(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate asset';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingAsset(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getBrandDNA = useCallback(async (): Promise<BrandDNATokens> => {
|
||||
setIsLoadingBrandDNA(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<{ brand_dna: BrandDNATokens }>('/api/campaign-creator/brand-dna');
|
||||
setBrandDNA(response.data.brand_dna);
|
||||
return response.data.brand_dna;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get brand DNA';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingBrandDNA(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getChannelBrandDNA = useCallback(
|
||||
async (channel: string): Promise<BrandDNATokens> => {
|
||||
setIsLoadingBrandDNA(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<{ channel: string; brand_dna: BrandDNATokens }>(
|
||||
`/api/campaign-creator/brand-dna/channel/${channel}`
|
||||
);
|
||||
return response.data.brand_dna;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get channel brand DNA';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingBrandDNA(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getChannelPack = useCallback(
|
||||
async (channel: string, assetType: string = 'social_post'): Promise<ChannelPack> => {
|
||||
setIsLoadingChannelPack(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<ChannelPack>(
|
||||
`/api/campaign-creator/channels/${channel}/pack?asset_type=${assetType}`
|
||||
);
|
||||
setChannelPack(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get channel pack';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingChannelPack(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const auditAsset = useCallback(
|
||||
async (imageBase64: string, assetMetadata?: any): Promise<AssetAuditResult> => {
|
||||
setIsAuditing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<AssetAuditResult>('/api/campaign-creator/assets/audit', {
|
||||
image_base64: imageBase64,
|
||||
asset_metadata: assetMetadata,
|
||||
});
|
||||
setAuditResult(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to audit asset';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsAuditing(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const listCampaigns = useCallback(async (status?: string): Promise<CampaignBlueprint[]> => {
|
||||
setIsLoadingCampaigns(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = status ? `/api/campaign-creator/campaigns?status=${status}` : '/api/campaign-creator/campaigns';
|
||||
const response = await aiApiClient.get<{ campaigns: CampaignBlueprint[]; total: number }>(url);
|
||||
setCampaigns(response.data.campaigns);
|
||||
return response.data.campaigns;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to list campaigns';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingCampaigns(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getCampaign = useCallback(async (campaignId: string): Promise<CampaignBlueprint> => {
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<CampaignBlueprint>(`/api/campaign-creator/campaigns/${campaignId}`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get campaign';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getCampaignProposals = useCallback(async (campaignId: string): Promise<AssetProposalsResponse> => {
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<AssetProposalsResponse>(`/api/campaign-creator/campaigns/${campaignId}/proposals`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get proposals';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const validateCampaignPreflight = useCallback(
|
||||
async (request: CampaignCreateRequest): Promise<PreflightValidationResult> => {
|
||||
setIsValidatingPreflight(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<PreflightValidationResult>(
|
||||
'/api/campaign-creator/campaigns/validate-preflight',
|
||||
request
|
||||
);
|
||||
setPreflightResult(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to validate campaign pre-flight';
|
||||
setError(errorMessage);
|
||||
const errorResult: PreflightValidationResult = {
|
||||
can_proceed: false,
|
||||
message: errorMessage,
|
||||
summary: {
|
||||
total_assets: 0,
|
||||
image_count: 0,
|
||||
text_count: 0,
|
||||
estimated_cost: 0,
|
||||
},
|
||||
};
|
||||
setPreflightResult(errorResult);
|
||||
return errorResult;
|
||||
} finally {
|
||||
setIsValidatingPreflight(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const clearBlueprint = useCallback(() => {
|
||||
setBlueprint(null);
|
||||
}, []);
|
||||
|
||||
const clearProposals = useCallback(() => {
|
||||
setProposals(null);
|
||||
}, []);
|
||||
|
||||
const clearAuditResult = useCallback(() => {
|
||||
setAuditResult(null);
|
||||
}, []);
|
||||
|
||||
// Personalization
|
||||
const [userPreferences, setUserPreferences] = useState<any>(null);
|
||||
const [isLoadingPreferences, setIsLoadingPreferences] = useState(false);
|
||||
const [recommendations, setRecommendations] = useState<any>(null);
|
||||
const [isLoadingRecommendations, setIsLoadingRecommendations] = useState(false);
|
||||
|
||||
const getPersonalizedDefaults = useCallback(
|
||||
async (formType: string): Promise<any> => {
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`);
|
||||
return response.data.defaults;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get personalized defaults';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getRecommendations = useCallback(async (): Promise<any> => {
|
||||
setIsLoadingRecommendations(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get('/api/product-marketing/personalization/recommendations');
|
||||
setRecommendations(response.data.recommendations);
|
||||
return response.data.recommendations;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get recommendations';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingRecommendations(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
error,
|
||||
blueprint,
|
||||
isCreatingBlueprint,
|
||||
proposals,
|
||||
isGeneratingProposals,
|
||||
brandDNA,
|
||||
isLoadingBrandDNA,
|
||||
channelPack,
|
||||
isLoadingChannelPack,
|
||||
auditResult,
|
||||
isAuditing,
|
||||
isGeneratingAsset,
|
||||
generatedAsset,
|
||||
preflightResult,
|
||||
isValidatingPreflight,
|
||||
campaigns,
|
||||
isLoadingCampaigns,
|
||||
|
||||
// Actions
|
||||
createCampaignBlueprint,
|
||||
generateAssetProposals,
|
||||
generateAsset,
|
||||
getBrandDNA,
|
||||
getChannelBrandDNA,
|
||||
getChannelPack,
|
||||
auditAsset,
|
||||
clearError,
|
||||
clearBlueprint,
|
||||
clearProposals,
|
||||
clearAuditResult,
|
||||
listCampaigns,
|
||||
getCampaign,
|
||||
getCampaignProposals,
|
||||
validateCampaignPreflight,
|
||||
|
||||
// Personalization
|
||||
getPersonalizedDefaults,
|
||||
getRecommendations,
|
||||
userPreferences,
|
||||
isLoadingPreferences,
|
||||
recommendations,
|
||||
isLoadingRecommendations,
|
||||
};
|
||||
};
|
||||
62
frontend/src/hooks/useCostEstimation.ts
Normal file
62
frontend/src/hooks/useCostEstimation.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { PreflightOperation } from '../services/billingService';
|
||||
|
||||
interface UseCostEstimationReturn {
|
||||
showEstimation: (operations: PreflightOperation[]) => void;
|
||||
estimationOperations: PreflightOperation[];
|
||||
isEstimationOpen: boolean;
|
||||
closeEstimation: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for cost estimation before operations.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { showEstimation, estimationOperations, isEstimationOpen, closeEstimation } = useCostEstimation();
|
||||
*
|
||||
* const handleGenerate = () => {
|
||||
* showEstimation([
|
||||
* {
|
||||
* provider: 'gemini',
|
||||
* model: 'gemini-2.5-flash',
|
||||
* operation_type: 'text_generation',
|
||||
* tokens_requested: 2000
|
||||
* }
|
||||
* ]);
|
||||
* };
|
||||
*
|
||||
* <CostEstimationModal
|
||||
* open={isEstimationOpen}
|
||||
* onClose={closeEstimation}
|
||||
* onConfirm={() => {
|
||||
* // Proceed with actual operation
|
||||
* performOperation();
|
||||
* }}
|
||||
* operations={estimationOperations}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const useCostEstimation = (): UseCostEstimationReturn => {
|
||||
const [isEstimationOpen, setIsEstimationOpen] = useState(false);
|
||||
const [estimationOperations, setEstimationOperations] = useState<PreflightOperation[]>([]);
|
||||
|
||||
const showEstimation = useCallback((operations: PreflightOperation[]) => {
|
||||
setEstimationOperations(operations);
|
||||
setIsEstimationOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeEstimation = useCallback(() => {
|
||||
setIsEstimationOpen(false);
|
||||
setEstimationOperations([]);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
showEstimation,
|
||||
estimationOperations,
|
||||
isEstimationOpen,
|
||||
closeEstimation,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCostEstimation;
|
||||
@@ -22,14 +22,26 @@ export const usePhaseNavigation = (
|
||||
seoRecommendationsApplied?: boolean
|
||||
) => {
|
||||
// Initialize from localStorage if available
|
||||
// If no research exists, default to empty string to show landing page
|
||||
// Only default to 'research' if research already exists (resuming a session)
|
||||
const getInitialPhase = (): string => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = window.localStorage.getItem('blogwriter_current_phase');
|
||||
if (stored) return stored;
|
||||
if (stored) {
|
||||
// If stored phase is 'research' but no research exists, show landing page instead
|
||||
if (stored === 'research' && !research) {
|
||||
return ''; // Return empty to show landing page
|
||||
}
|
||||
// For other phases, use stored value (user might be in middle of outline/content/seo/publish)
|
||||
// Even if research doesn't exist, allow other phases to be restored (edge case)
|
||||
return stored;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return 'research';
|
||||
// Default to empty string to show landing page when no research exists
|
||||
// Will be set to 'research' when user clicks "Start Research"
|
||||
return research ? 'research' : '';
|
||||
};
|
||||
|
||||
const [currentPhase, setCurrentPhase] = useState<string>(getInitialPhase());
|
||||
@@ -113,25 +125,38 @@ export const usePhaseNavigation = (
|
||||
|
||||
// Validate stored phase against current availability (quiet)
|
||||
useEffect(() => {
|
||||
// Allow empty string as a valid phase (landing page state)
|
||||
if (currentPhase === '') {
|
||||
return; // Don't validate empty phase - it's intentional for landing page
|
||||
}
|
||||
|
||||
const current = phases.find(p => p.id === currentPhase);
|
||||
if (!current) {
|
||||
setCurrentPhase('research');
|
||||
// If phase not found and no research exists, go to landing (empty string)
|
||||
// Otherwise, default to research
|
||||
setCurrentPhase(research ? 'research' : '');
|
||||
return;
|
||||
}
|
||||
if (current.disabled) {
|
||||
// Find the first non-disabled phase in order of progression the user qualifies for
|
||||
const fallback = phases.find(p => !p.disabled) || ({ id: 'research' } as Phase);
|
||||
// If no research exists, default to landing (empty string) instead of research
|
||||
const fallback = phases.find(p => !p.disabled) || ({ id: research ? 'research' : '' } as Phase);
|
||||
if (fallback.id !== currentPhase) {
|
||||
setCurrentPhase(fallback.id);
|
||||
}
|
||||
}
|
||||
}, [phases, currentPhase]);
|
||||
}, [phases, currentPhase, research]);
|
||||
|
||||
// Auto-update current phase based on completion status (only if user hasn't manually selected a phase)
|
||||
useEffect(() => {
|
||||
if (userSelectedPhase) {
|
||||
return; // Don't auto-update if user has manually selected a phase
|
||||
}
|
||||
|
||||
// If no research exists and phase is empty/landing, stay on landing
|
||||
if (!research && currentPhase === '') {
|
||||
return; // Keep showing landing page
|
||||
}
|
||||
|
||||
// Auto-progress to the next available phase when conditions are met
|
||||
if (research && outline.length === 0) {
|
||||
|
||||
@@ -261,6 +261,11 @@ export function useResearchPolling(options: UsePollingOptions = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
export function useBlogWriterResearchPolling(options: UsePollingOptions = {}) {
|
||||
// Use Blog Writer research polling endpoint - direct import (already imported at top)
|
||||
return usePolling(blogWriterApi.pollResearchStatus, options);
|
||||
}
|
||||
|
||||
export function useOutlinePolling(options: UsePollingOptions = {}) {
|
||||
return usePolling(blogWriterApi.pollOutlineStatus, options);
|
||||
}
|
||||
|
||||
251
frontend/src/hooks/usePriority2Alerts.ts
Normal file
251
frontend/src/hooks/usePriority2Alerts.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Priority 2 Alert System Hook
|
||||
*
|
||||
* Integrates Priority 2 features from cost transparency review as alerts:
|
||||
* - Dynamic Pricing Display alerts (pricing changes, OSS model recommendations)
|
||||
* - Cost Estimation Before Operations alerts (high-cost operation warnings)
|
||||
* - Historical Cost Trends alerts (spending velocity, projection warnings)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { billingService } from '../services/billingService';
|
||||
import { DashboardData } from '../types/billing';
|
||||
import { showToastNotification } from '../utils/toastNotifications';
|
||||
|
||||
export interface Priority2Alert {
|
||||
id: string;
|
||||
type: 'pricing_change' | 'cost_estimation' | 'cost_trend' | 'oss_recommendation';
|
||||
severity: 'info' | 'warning' | 'error';
|
||||
title: string;
|
||||
message: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
dismissible?: boolean;
|
||||
}
|
||||
|
||||
interface UsePriority2AlertsOptions {
|
||||
userId?: string;
|
||||
enabled?: boolean;
|
||||
checkInterval?: number; // milliseconds
|
||||
}
|
||||
|
||||
interface UsePriority2AlertsReturn {
|
||||
alerts: Priority2Alert[];
|
||||
isLoading: boolean;
|
||||
refreshAlerts: () => Promise<void>;
|
||||
dismissAlert: (alertId: string) => void;
|
||||
}
|
||||
|
||||
const ALERT_CHECK_INTERVAL = 60000; // 1 minute default
|
||||
|
||||
export const usePriority2Alerts = (
|
||||
options: UsePriority2AlertsOptions = {}
|
||||
): UsePriority2AlertsReturn => {
|
||||
const {
|
||||
userId,
|
||||
enabled = true,
|
||||
checkInterval = ALERT_CHECK_INTERVAL
|
||||
} = options;
|
||||
|
||||
const [alerts, setAlerts] = useState<Priority2Alert[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||
const [lastCheck, setLastCheck] = useState<Date | null>(null);
|
||||
|
||||
const generateAlerts = useCallback((data: DashboardData): Priority2Alert[] => {
|
||||
const generatedAlerts: Priority2Alert[] = [];
|
||||
const currentUsage = data.current_usage;
|
||||
const limits = data.limits;
|
||||
const projections = data.projections;
|
||||
|
||||
if (!currentUsage || !limits) return generatedAlerts;
|
||||
|
||||
// 1. Cost Trend Alerts (Priority 2: Historical Cost Trends)
|
||||
const costLimit = limits.limits?.monthly_cost || 0;
|
||||
const currentCost = currentUsage.total_cost || 0;
|
||||
const projectedCost = projections?.projected_monthly_cost || 0;
|
||||
const costUsagePercentage = costLimit > 0 ? (currentCost / costLimit) * 100 : 0;
|
||||
const projectedPercentage = costLimit > 0 ? (projectedCost / costLimit) * 100 : 0;
|
||||
|
||||
// High spending velocity alert
|
||||
if (projectedPercentage > 120 && costUsagePercentage < 80) {
|
||||
const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
|
||||
const currentDay = new Date().getDate();
|
||||
const avgDailyCost = currentCost / currentDay;
|
||||
const daysUntilExhaustion = costLimit > avgDailyCost
|
||||
? Math.ceil((costLimit - currentCost) / avgDailyCost)
|
||||
: 0;
|
||||
|
||||
generatedAlerts.push({
|
||||
id: 'cost-velocity-high',
|
||||
type: 'cost_trend',
|
||||
severity: 'warning',
|
||||
title: 'High Spending Velocity Detected',
|
||||
message: `Your current spending rate projects to ${projectedCost.toFixed(2)} this month (${projectedPercentage.toFixed(0)}% of limit). At this rate, you'll exhaust your budget in ~${daysUntilExhaustion} days.`,
|
||||
action: {
|
||||
label: 'View Cost Trends',
|
||||
onClick: () => {
|
||||
// Navigate to billing dashboard
|
||||
window.location.href = '/billing';
|
||||
}
|
||||
},
|
||||
dismissible: true
|
||||
});
|
||||
}
|
||||
|
||||
// Cost projection warning
|
||||
if (projectedPercentage >= 95 && costUsagePercentage < 95) {
|
||||
generatedAlerts.push({
|
||||
id: 'cost-projection-critical',
|
||||
type: 'cost_trend',
|
||||
severity: 'error',
|
||||
title: 'Critical: Budget Exhaustion Projected',
|
||||
message: `Based on current spending, you're projected to exceed your $${costLimit.toFixed(2)} monthly budget. Current: $${currentCost.toFixed(2)} (${costUsagePercentage.toFixed(0)}%), Projected: $${projectedCost.toFixed(2)} (${projectedPercentage.toFixed(0)}%)`,
|
||||
action: {
|
||||
label: 'Upgrade Plan',
|
||||
onClick: () => {
|
||||
window.location.href = '/subscription';
|
||||
}
|
||||
},
|
||||
dismissible: false
|
||||
});
|
||||
}
|
||||
|
||||
// 2. OSS Model Recommendation Alerts (Priority 2: Dynamic Pricing Display)
|
||||
// Check if user is using expensive models when cheaper OSS alternatives exist
|
||||
const providerBreakdown = currentUsage.provider_breakdown || {};
|
||||
// ProviderBreakdown type may not include all providers, so use type assertion for dynamic access
|
||||
const stabilityCost = (providerBreakdown as any).stability?.cost || 0;
|
||||
const stabilityCalls = (providerBreakdown as any).stability?.calls || 0;
|
||||
|
||||
// If using Stability AI for images, recommend OSS alternative
|
||||
if (stabilityCalls > 10 && stabilityCost > 0.5) {
|
||||
const ossSavings = (stabilityCost * 0.25).toFixed(2); // 25% savings with OSS
|
||||
generatedAlerts.push({
|
||||
id: 'oss-image-recommendation',
|
||||
type: 'oss_recommendation',
|
||||
severity: 'info',
|
||||
title: '💡 Cost Savings Opportunity',
|
||||
message: `You've spent $${stabilityCost.toFixed(2)} on image generation. Switch to Qwen Image OSS model to save ~$${ossSavings} (25% cheaper at $0.03/image vs $0.04/image).`,
|
||||
action: {
|
||||
label: 'Learn More',
|
||||
onClick: () => {
|
||||
// Could open a modal or navigate to pricing page
|
||||
showToastNotification('OSS models are automatically used as defaults in Basic tier', 'info');
|
||||
}
|
||||
},
|
||||
dismissible: true
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Cost Estimation Alerts (Priority 2: Cost Estimation Before Operations)
|
||||
// These are generated contextually when operations are about to be performed
|
||||
// This hook provides the infrastructure, but alerts are triggered by components
|
||||
|
||||
return generatedAlerts;
|
||||
}, []);
|
||||
|
||||
const refreshAlerts = useCallback(async () => {
|
||||
if (!enabled || !userId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const data = await billingService.getDashboardData(userId);
|
||||
setDashboardData(data);
|
||||
|
||||
const newAlerts = generateAlerts(data);
|
||||
setAlerts(newAlerts);
|
||||
setLastCheck(new Date());
|
||||
|
||||
// Show toast for critical alerts
|
||||
const criticalAlerts = newAlerts.filter(a => a.severity === 'error');
|
||||
if (criticalAlerts.length > 0) {
|
||||
criticalAlerts.forEach(alert => {
|
||||
showToastNotification(alert.message, 'error', { duration: 8000 });
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Priority2Alerts] Error refreshing alerts:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [enabled, userId, generateAlerts]);
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
if (enabled && userId) {
|
||||
refreshAlerts();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enabled, userId]); // Only run on mount or when enabled/userId changes
|
||||
|
||||
// Periodic refresh
|
||||
useEffect(() => {
|
||||
if (!enabled || !userId) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refreshAlerts();
|
||||
}, checkInterval);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [enabled, userId, checkInterval]);
|
||||
|
||||
const dismissAlert = useCallback((alertId: string) => {
|
||||
setAlerts(prev => prev.filter(alert => {
|
||||
if (alert.id === alertId && alert.dismissible) {
|
||||
// Store dismissed alert ID in localStorage to prevent re-showing
|
||||
const dismissed = JSON.parse(localStorage.getItem('dismissedPriority2Alerts') || '[]');
|
||||
dismissed.push(alertId);
|
||||
localStorage.setItem('dismissedPriority2Alerts', JSON.stringify(dismissed));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Filter out dismissed alerts
|
||||
useEffect(() => {
|
||||
const dismissed = JSON.parse(localStorage.getItem('dismissedPriority2Alerts') || '[]');
|
||||
setAlerts(prev => prev.filter(alert => !dismissed.includes(alert.id)));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
alerts,
|
||||
isLoading,
|
||||
refreshAlerts,
|
||||
dismissAlert
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for generating cost estimation alerts before operations
|
||||
* Used by components to show warnings before expensive operations
|
||||
*/
|
||||
export const useCostEstimationAlert = () => {
|
||||
const showEstimationAlert = useCallback((
|
||||
estimatedCost: number,
|
||||
operationType: string,
|
||||
onProceed: () => void,
|
||||
onCancel: () => void
|
||||
) => {
|
||||
const costLimit = 45; // Basic tier limit - could be fetched from subscription
|
||||
const costPercentage = (estimatedCost / costLimit) * 100;
|
||||
|
||||
if (estimatedCost > 1.0 || costPercentage > 5) {
|
||||
// High-cost operation warning
|
||||
const severity = costPercentage > 10 ? 'error' : 'warning';
|
||||
const message = `This ${operationType} will cost approximately $${estimatedCost.toFixed(4)}. ` +
|
||||
`This represents ${costPercentage.toFixed(1)}% of your monthly budget.`;
|
||||
|
||||
showToastNotification(message, severity, {
|
||||
duration: 10000
|
||||
});
|
||||
// Note: Toast doesn't support actions - user can proceed via the operation button
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { showEstimationAlert };
|
||||
};
|
||||
@@ -1,416 +1,15 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { aiApiClient } from '../api/client';
|
||||
|
||||
export interface CampaignCreateRequest {
|
||||
campaign_name: string;
|
||||
goal: string;
|
||||
kpi?: string;
|
||||
channels: string[];
|
||||
product_context?: {
|
||||
product_description?: string;
|
||||
product_name?: string;
|
||||
marketing_goal?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CampaignBlueprint {
|
||||
campaign_id: string;
|
||||
campaign_name: string;
|
||||
goal: string;
|
||||
kpi?: string;
|
||||
phases: Array<{
|
||||
name: string;
|
||||
duration_days: number;
|
||||
purpose: string;
|
||||
}>;
|
||||
asset_nodes: Array<{
|
||||
asset_id: string;
|
||||
asset_type: string;
|
||||
channel: string;
|
||||
status: string;
|
||||
}>;
|
||||
channels: string[];
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface AssetProposal {
|
||||
asset_id: string;
|
||||
asset_type: string;
|
||||
channel: string;
|
||||
proposed_prompt: string;
|
||||
recommended_template?: string;
|
||||
recommended_provider?: string;
|
||||
cost_estimate: number;
|
||||
concept_summary: string;
|
||||
}
|
||||
|
||||
export interface AssetProposalsResponse {
|
||||
proposals: Record<string, AssetProposal>;
|
||||
total_assets: number;
|
||||
}
|
||||
|
||||
export interface BrandDNATokens {
|
||||
writing_style: {
|
||||
tone: string;
|
||||
voice: string;
|
||||
complexity: string;
|
||||
engagement_level: string;
|
||||
};
|
||||
target_audience: {
|
||||
demographics: string[];
|
||||
industry_focus: string;
|
||||
expertise_level: string;
|
||||
};
|
||||
visual_identity: {
|
||||
color_palette?: string[];
|
||||
brand_values?: string[];
|
||||
positioning?: string;
|
||||
style_guidelines?: any;
|
||||
};
|
||||
persona: {
|
||||
persona_name?: string;
|
||||
archetype?: string;
|
||||
core_belief?: string;
|
||||
linguistic_fingerprint?: any;
|
||||
platform_personas?: any;
|
||||
};
|
||||
competitive_positioning: {
|
||||
differentiators: string[];
|
||||
unique_value_props: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChannelPack {
|
||||
channel: string;
|
||||
platform: string;
|
||||
asset_type: string;
|
||||
templates: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
dimensions: string;
|
||||
aspect_ratio: string;
|
||||
recommended_provider: string;
|
||||
quality: string;
|
||||
}>;
|
||||
formats: Array<{
|
||||
name: string;
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: string;
|
||||
safe_zone?: any;
|
||||
}>;
|
||||
copy_framework: Record<string, any>;
|
||||
optimization_tips: string[];
|
||||
}
|
||||
|
||||
export interface AssetAuditResult {
|
||||
asset_info: {
|
||||
width: number;
|
||||
height: number;
|
||||
format: string;
|
||||
mode: string;
|
||||
quality_score: number;
|
||||
};
|
||||
recommendations: Array<{
|
||||
operation: string;
|
||||
priority: string;
|
||||
reason: string;
|
||||
suggested_mode?: string;
|
||||
suggested_format?: string;
|
||||
suggested_operations?: string[];
|
||||
}>;
|
||||
status: 'usable' | 'needs_enhancement' | 'error';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PreflightValidationResult {
|
||||
can_proceed: boolean;
|
||||
message?: string;
|
||||
error_details?: Record<string, any>;
|
||||
summary: {
|
||||
total_assets: number;
|
||||
image_count: number;
|
||||
text_count: number;
|
||||
estimated_cost: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* useProductMarketing Hook
|
||||
*
|
||||
* Hook for Product Marketing Suite - Product asset generation only.
|
||||
* For campaign management, use useCampaignCreator hook instead.
|
||||
*/
|
||||
export const useProductMarketing = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Campaign Blueprint
|
||||
const [blueprint, setBlueprint] = useState<CampaignBlueprint | null>(null);
|
||||
const [isCreatingBlueprint, setIsCreatingBlueprint] = useState(false);
|
||||
|
||||
// Asset Proposals
|
||||
const [proposals, setProposals] = useState<AssetProposalsResponse | null>(null);
|
||||
const [isGeneratingProposals, setIsGeneratingProposals] = useState(false);
|
||||
|
||||
// Brand DNA
|
||||
const [brandDNA, setBrandDNA] = useState<BrandDNATokens | null>(null);
|
||||
const [isLoadingBrandDNA, setIsLoadingBrandDNA] = useState(false);
|
||||
|
||||
// Channel Packs
|
||||
const [channelPack, setChannelPack] = useState<ChannelPack | null>(null);
|
||||
const [isLoadingChannelPack, setIsLoadingChannelPack] = useState(false);
|
||||
|
||||
// Asset Audit
|
||||
const [auditResult, setAuditResult] = useState<AssetAuditResult | null>(null);
|
||||
const [isAuditing, setIsAuditing] = useState(false);
|
||||
|
||||
// Asset Generation
|
||||
const [isGeneratingAsset, setIsGeneratingAsset] = useState(false);
|
||||
const [generatedAsset, setGeneratedAsset] = useState<any>(null);
|
||||
|
||||
// Pre-flight Validation
|
||||
const [preflightResult, setPreflightResult] = useState<PreflightValidationResult | null>(null);
|
||||
const [isValidatingPreflight, setIsValidatingPreflight] = useState(false);
|
||||
|
||||
const createCampaignBlueprint = useCallback(
|
||||
async (request: CampaignCreateRequest): Promise<CampaignBlueprint> => {
|
||||
setIsCreatingBlueprint(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<CampaignBlueprint>(
|
||||
'/api/product-marketing/campaigns/create-blueprint',
|
||||
request
|
||||
);
|
||||
setBlueprint(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create campaign blueprint';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsCreatingBlueprint(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const generateAssetProposals = useCallback(
|
||||
async (campaignId: string, productContext?: any): Promise<AssetProposalsResponse> => {
|
||||
setIsGeneratingProposals(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<AssetProposalsResponse>(
|
||||
`/api/product-marketing/campaigns/${campaignId}/generate-proposals`,
|
||||
{
|
||||
campaign_id: campaignId,
|
||||
product_context: productContext,
|
||||
}
|
||||
);
|
||||
setProposals(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate asset proposals';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingProposals(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const generateAsset = useCallback(
|
||||
async (assetProposal: AssetProposal, productContext?: any): Promise<any> => {
|
||||
setIsGeneratingAsset(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post('/api/product-marketing/assets/generate', {
|
||||
asset_proposal: assetProposal,
|
||||
product_context: productContext,
|
||||
});
|
||||
setGeneratedAsset(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate asset';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingAsset(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getBrandDNA = useCallback(async (): Promise<BrandDNATokens> => {
|
||||
setIsLoadingBrandDNA(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<{ brand_dna: BrandDNATokens }>('/api/product-marketing/brand-dna');
|
||||
setBrandDNA(response.data.brand_dna);
|
||||
return response.data.brand_dna;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get brand DNA';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingBrandDNA(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getChannelBrandDNA = useCallback(
|
||||
async (channel: string): Promise<BrandDNATokens> => {
|
||||
setIsLoadingBrandDNA(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<{ channel: string; brand_dna: BrandDNATokens }>(
|
||||
`/api/product-marketing/brand-dna/channel/${channel}`
|
||||
);
|
||||
return response.data.brand_dna;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get channel brand DNA';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingBrandDNA(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getChannelPack = useCallback(
|
||||
async (channel: string, assetType: string = 'social_post'): Promise<ChannelPack> => {
|
||||
setIsLoadingChannelPack(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<ChannelPack>(
|
||||
`/api/product-marketing/channels/${channel}/pack?asset_type=${assetType}`
|
||||
);
|
||||
setChannelPack(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get channel pack';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingChannelPack(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const auditAsset = useCallback(
|
||||
async (imageBase64: string, assetMetadata?: any): Promise<AssetAuditResult> => {
|
||||
setIsAuditing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<AssetAuditResult>('/api/product-marketing/assets/audit', {
|
||||
image_base64: imageBase64,
|
||||
asset_metadata: assetMetadata,
|
||||
});
|
||||
setAuditResult(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to audit asset';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsAuditing(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const clearBlueprint = useCallback(() => {
|
||||
setBlueprint(null);
|
||||
}, []);
|
||||
|
||||
const clearProposals = useCallback(() => {
|
||||
setProposals(null);
|
||||
}, []);
|
||||
|
||||
const clearAuditResult = useCallback(() => {
|
||||
setAuditResult(null);
|
||||
}, []);
|
||||
|
||||
// Campaign listing
|
||||
const [campaigns, setCampaigns] = useState<CampaignBlueprint[]>([]);
|
||||
const [isLoadingCampaigns, setIsLoadingCampaigns] = useState(false);
|
||||
|
||||
const listCampaigns = useCallback(async (status?: string): Promise<CampaignBlueprint[]> => {
|
||||
setIsLoadingCampaigns(true);
|
||||
setError(null);
|
||||
try {
|
||||
const url = status ? `/api/product-marketing/campaigns?status=${status}` : '/api/product-marketing/campaigns';
|
||||
const response = await aiApiClient.get<{ campaigns: CampaignBlueprint[]; total: number }>(url);
|
||||
setCampaigns(response.data.campaigns);
|
||||
return response.data.campaigns;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to list campaigns';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingCampaigns(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getCampaign = useCallback(async (campaignId: string): Promise<CampaignBlueprint> => {
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<CampaignBlueprint>(`/api/product-marketing/campaigns/${campaignId}`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get campaign';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getCampaignProposals = useCallback(async (campaignId: string): Promise<AssetProposalsResponse> => {
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get<AssetProposalsResponse>(`/api/product-marketing/campaigns/${campaignId}/proposals`);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get proposals';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const validateCampaignPreflight = useCallback(
|
||||
async (request: CampaignCreateRequest): Promise<PreflightValidationResult> => {
|
||||
setIsValidatingPreflight(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post<PreflightValidationResult>(
|
||||
'/api/product-marketing/campaigns/validate-preflight',
|
||||
request
|
||||
);
|
||||
setPreflightResult(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to validate campaign pre-flight';
|
||||
setError(errorMessage);
|
||||
// Return error result
|
||||
const errorResult: PreflightValidationResult = {
|
||||
can_proceed: false,
|
||||
message: errorMessage,
|
||||
summary: {
|
||||
total_assets: 0,
|
||||
image_count: 0,
|
||||
text_count: 0,
|
||||
estimated_cost: 0,
|
||||
},
|
||||
};
|
||||
setPreflightResult(errorResult);
|
||||
return errorResult;
|
||||
} finally {
|
||||
setIsValidatingPreflight(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Product Image Generation (Product Marketing Suite - Product Assets)
|
||||
const [isGeneratingProductImage, setIsGeneratingProductImage] = useState(false);
|
||||
const [generatedProductImage, setGeneratedProductImage] = useState<any>(null);
|
||||
@@ -448,49 +47,262 @@ export const useProductMarketing = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
// Product Animation Generation (Image-to-Video)
|
||||
const [isGeneratingAnimation, setIsGeneratingAnimation] = useState(false);
|
||||
const [generatedAnimation, setGeneratedAnimation] = useState<any>(null);
|
||||
const [animationError, setAnimationError] = useState<string | null>(null);
|
||||
|
||||
const generateProductAnimation = useCallback(
|
||||
async (request: {
|
||||
product_image_base64: string;
|
||||
animation_type: string;
|
||||
product_name: string;
|
||||
product_description?: string;
|
||||
resolution?: string;
|
||||
duration?: number;
|
||||
audio_base64?: string;
|
||||
additional_context?: string;
|
||||
}): Promise<any> => {
|
||||
setIsGeneratingAnimation(true);
|
||||
setAnimationError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post('/api/product-marketing/products/animate', request);
|
||||
setGeneratedAnimation(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product animation';
|
||||
setAnimationError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingAnimation(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Product Video Generation (Text-to-Video)
|
||||
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
|
||||
const [generatedVideo, setGeneratedVideo] = useState<any>(null);
|
||||
const [videoError, setVideoError] = useState<string | null>(null);
|
||||
|
||||
const generateProductVideo = useCallback(
|
||||
async (request: {
|
||||
product_name: string;
|
||||
product_description: string;
|
||||
video_type: string;
|
||||
resolution?: string;
|
||||
duration?: number;
|
||||
audio_base64?: string;
|
||||
additional_context?: string;
|
||||
}): Promise<any> => {
|
||||
setIsGeneratingVideo(true);
|
||||
setVideoError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post('/api/product-marketing/products/video/demo', request);
|
||||
setGeneratedVideo(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product video';
|
||||
setVideoError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingVideo(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Product Avatar Generation (Talking Avatar)
|
||||
const [isGeneratingAvatar, setIsGeneratingAvatar] = useState(false);
|
||||
const [generatedAvatar, setGeneratedAvatar] = useState<any>(null);
|
||||
const [avatarError, setAvatarError] = useState<string | null>(null);
|
||||
|
||||
const generateProductAvatar = useCallback(
|
||||
async (request: {
|
||||
avatar_image_base64: string;
|
||||
script_text?: string;
|
||||
audio_base64?: string;
|
||||
product_name: string;
|
||||
product_description?: string;
|
||||
explainer_type?: string;
|
||||
resolution?: string;
|
||||
prompt?: string;
|
||||
mask_image_base64?: string;
|
||||
additional_context?: string;
|
||||
}): Promise<any> => {
|
||||
setIsGeneratingAvatar(true);
|
||||
setAvatarError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post('/api/product-marketing/products/avatar/explainer', request);
|
||||
setGeneratedAvatar(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to generate product avatar';
|
||||
setAvatarError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsGeneratingAvatar(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Intelligent Prompt Inference
|
||||
const [isInferringPrompt, setIsInferringPrompt] = useState(false);
|
||||
const [inferredConfig, setInferredConfig] = useState<any>(null);
|
||||
const [inferenceError, setInferenceError] = useState<string | null>(null);
|
||||
|
||||
// Brand DNA
|
||||
const [brandDNA, setBrandDNA] = useState<any>(null);
|
||||
const [isLoadingBrandDNA, setIsLoadingBrandDNA] = useState(false);
|
||||
|
||||
// Personalization
|
||||
const [userPreferences, setUserPreferences] = useState<any>(null);
|
||||
const [isLoadingPreferences, setIsLoadingPreferences] = useState(false);
|
||||
const [recommendations, setRecommendations] = useState<any>(null);
|
||||
const [isLoadingRecommendations, setIsLoadingRecommendations] = useState(false);
|
||||
|
||||
const inferRequirements = useCallback(
|
||||
async (userInput: string, assetType?: string): Promise<any> => {
|
||||
setIsInferringPrompt(true);
|
||||
setInferenceError(null);
|
||||
try {
|
||||
const response = await aiApiClient.post('/api/product-marketing/intelligent-prompt', {
|
||||
user_input: userInput,
|
||||
asset_type: assetType,
|
||||
});
|
||||
setInferredConfig(response.data.configuration);
|
||||
return response.data.configuration;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to infer requirements';
|
||||
setInferenceError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsInferringPrompt(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getBrandDNA = useCallback(async (): Promise<any> => {
|
||||
setIsLoadingBrandDNA(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Brand DNA is shared between campaign creator and product marketing
|
||||
// Using campaign-creator endpoint since it's the same data
|
||||
const response = await aiApiClient.get('/api/campaign-creator/brand-dna');
|
||||
setBrandDNA(response.data.brand_dna);
|
||||
return response.data.brand_dna;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get brand DNA';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingBrandDNA(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getUserPreferences = useCallback(async (): Promise<any> => {
|
||||
setIsLoadingPreferences(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get('/api/product-marketing/personalization/preferences');
|
||||
setUserPreferences(response.data.preferences);
|
||||
return response.data.preferences;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get user preferences';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingPreferences(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPersonalizedDefaults = useCallback(
|
||||
async (formType: string): Promise<any> => {
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get(`/api/product-marketing/personalization/defaults/${formType}`);
|
||||
return response.data.defaults;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get personalized defaults';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getRecommendations = useCallback(async (): Promise<any> => {
|
||||
setIsLoadingRecommendations(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await aiApiClient.get('/api/product-marketing/personalization/recommendations');
|
||||
setRecommendations(response.data.recommendations);
|
||||
return response.data.recommendations;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to get recommendations';
|
||||
setError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
setIsLoadingRecommendations(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setError(null);
|
||||
setInferenceError(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
isLoading,
|
||||
error,
|
||||
blueprint,
|
||||
isCreatingBlueprint,
|
||||
proposals,
|
||||
isGeneratingProposals,
|
||||
brandDNA,
|
||||
isLoadingBrandDNA,
|
||||
channelPack,
|
||||
isLoadingChannelPack,
|
||||
auditResult,
|
||||
isAuditing,
|
||||
isGeneratingAsset,
|
||||
generatedAsset,
|
||||
preflightResult,
|
||||
isValidatingPreflight,
|
||||
|
||||
// Actions
|
||||
createCampaignBlueprint,
|
||||
generateAssetProposals,
|
||||
generateAsset,
|
||||
getBrandDNA,
|
||||
getChannelBrandDNA,
|
||||
getChannelPack,
|
||||
auditAsset,
|
||||
clearError,
|
||||
clearBlueprint,
|
||||
clearProposals,
|
||||
clearAuditResult,
|
||||
campaigns,
|
||||
isLoadingCampaigns,
|
||||
listCampaigns,
|
||||
getCampaign,
|
||||
getCampaignProposals,
|
||||
validateCampaignPreflight,
|
||||
|
||||
// Product Image Generation (Product Marketing Suite)
|
||||
// Product Image Generation
|
||||
generateProductImage,
|
||||
isGeneratingProductImage,
|
||||
generatedProductImage,
|
||||
productImageError,
|
||||
|
||||
// Product Animation Generation (Image-to-Video)
|
||||
generateProductAnimation,
|
||||
isGeneratingAnimation,
|
||||
generatedAnimation,
|
||||
animationError,
|
||||
|
||||
// Product Video Generation (Text-to-Video)
|
||||
generateProductVideo,
|
||||
isGeneratingVideo,
|
||||
generatedVideo,
|
||||
videoError,
|
||||
|
||||
// Product Avatar Generation (Talking Avatar)
|
||||
generateProductAvatar,
|
||||
isGeneratingAvatar,
|
||||
generatedAvatar,
|
||||
avatarError,
|
||||
|
||||
// Intelligent Prompt Inference
|
||||
inferRequirements,
|
||||
isInferringPrompt,
|
||||
inferredConfig,
|
||||
inferenceError,
|
||||
|
||||
// Brand DNA
|
||||
brandDNA,
|
||||
getBrandDNA,
|
||||
isLoadingBrandDNA,
|
||||
|
||||
// Personalization
|
||||
getUserPreferences,
|
||||
userPreferences,
|
||||
isLoadingPreferences,
|
||||
getPersonalizedDefaults,
|
||||
getRecommendations,
|
||||
recommendations,
|
||||
isLoadingRecommendations,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user