Fix LinkedIn writer: progress animation, persona API 404 handling, back-to-home navigation
- Simulate progress step advancement at 1.5s intervals during API calls so users see incremental progress instead of all-at-once bursts - PersonaChip skips API calls entirely in feature-only mode (no console spam) - getUserPersonas/getPlatformPersona return null on 404 instead of throwing - PersonaChip shows neutral gray state when no persona data exists - Back button now clears draft to return to LinkedIn writer home screen - Article title extracted from markdown content (fixes KeyError) - InitialRouteHandler: demo mode subscribes getDefaultLandingRoute() - Header: back button shown when draft exists, navigates to home screen
This commit is contained in:
@@ -478,8 +478,25 @@ class ContentGenerator:
|
|||||||
|
|
||||||
content_text = raw_response if isinstance(raw_response, str) else str(raw_response or "")
|
content_text = raw_response if isinstance(raw_response, str) else str(raw_response or "")
|
||||||
|
|
||||||
|
# Extract title from article content (first markdown heading or first line)
|
||||||
|
title = ""
|
||||||
|
for line in content_text.split('\n'):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith('# '):
|
||||||
|
title = stripped[2:].strip()
|
||||||
|
break
|
||||||
|
if not title:
|
||||||
|
for line in content_text.split('\n'):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped:
|
||||||
|
title = stripped[:100].strip()
|
||||||
|
break
|
||||||
|
if not title:
|
||||||
|
title = request.topic or "LinkedIn Article"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'content': content_text,
|
'content': content_text,
|
||||||
|
'title': title,
|
||||||
'sources': [],
|
'sources': [],
|
||||||
'citations': [],
|
'citations': [],
|
||||||
'grounding_enabled': bool(research_sources),
|
'grounding_enabled': bool(research_sources),
|
||||||
|
|||||||
@@ -121,11 +121,12 @@ export const generateWritingPersona = async (userId: number = 1, request: Person
|
|||||||
* Get all writing personas for a user
|
* Get all writing personas for a user
|
||||||
* Note: user_id is extracted from Clerk JWT token, no need to pass it
|
* Note: user_id is extracted from Clerk JWT token, no need to pass it
|
||||||
*/
|
*/
|
||||||
export const getUserPersonas = async (): Promise<{ personas: PersonaResponse[]; total_count: number }> => {
|
export const getUserPersonas = async (): Promise<{ personas: PersonaResponse[]; total_count: number } | null> => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/api/personas/user');
|
const response = await apiClient.get('/api/personas/user');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 404) return null;
|
||||||
console.error('Error getting user personas:', error);
|
console.error('Error getting user personas:', error);
|
||||||
throw new Error(error.response?.data?.detail || 'Failed to get user personas');
|
throw new Error(error.response?.data?.detail || 'Failed to get user personas');
|
||||||
}
|
}
|
||||||
@@ -155,6 +156,7 @@ export const getPlatformPersona = async (platform: string): Promise<any> => {
|
|||||||
const response = await apiClient.get(`/api/personas/platform/${platform}`);
|
const response = await apiClient.get(`/api/personas/platform/${platform}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 404) return null;
|
||||||
console.error('Error getting platform persona:', error);
|
console.error('Error getting platform persona:', error);
|
||||||
throw new Error(error.response?.data?.detail || 'Failed to get platform persona');
|
throw new Error(error.response?.data?.detail || 'Failed to get platform persona');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,12 +145,14 @@ const InitialRouteHandler: React.FC = () => {
|
|||||||
return navigateAndLog(redirectTo);
|
return navigateAndLog(redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feature-only mode (e.g., ALWRITY_ENABLED_FEATURES=linkedin)
|
||||||
if (shouldSkipOnboarding()) {
|
if (shouldSkipOnboarding()) {
|
||||||
const route = getDefaultLandingRoute();
|
const route = getDefaultLandingRoute();
|
||||||
console.log(`InitialRouteHandler: Checkout success in demo mode → ${route}`);
|
console.log(`InitialRouteHandler: Checkout success — feature-only mode → ${route}`);
|
||||||
return navigateAndLog(route);
|
return navigateAndLog(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Full mode: check if onboarding is needed
|
||||||
if (!isOnboardingComplete) {
|
if (!isOnboardingComplete) {
|
||||||
console.log('InitialRouteHandler: Checkout success — onboarding incomplete → Onboarding');
|
console.log('InitialRouteHandler: Checkout success — onboarding incomplete → Onboarding');
|
||||||
return navigateAndLog('/onboarding');
|
return navigateAndLog('/onboarding');
|
||||||
@@ -299,9 +301,9 @@ const InitialRouteHandler: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSkipOnboarding()) {
|
if (shouldSkipOnboarding()) {
|
||||||
const route = getDefaultLandingRoute();
|
// Feature-only mode still requires subscription
|
||||||
console.log(`InitialRouteHandler: Demo mode - no subscription but allowing access to ${route}`);
|
console.log('InitialRouteHandler: No subscription data in feature-only mode → Pricing page');
|
||||||
return navigateAndLog(route);
|
return navigateAndLog("/pricing");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('InitialRouteHandler: No subscription data after check → Pricing page');
|
console.log('InitialRouteHandler: No subscription data after check → Pricing page');
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
|
|||||||
// Handlers
|
// Handlers
|
||||||
handleDraftChange,
|
handleDraftChange,
|
||||||
handleContextChange,
|
handleContextChange,
|
||||||
// handleClear,
|
handleClear,
|
||||||
// handleCopy,
|
// handleCopy,
|
||||||
handleClearHistory,
|
handleClearHistory,
|
||||||
|
|
||||||
@@ -438,6 +438,8 @@ Always use the most appropriate tool for the user's request.`.trim();
|
|||||||
onPreferencesChange={handlePreferencesChange}
|
onPreferencesChange={handlePreferencesChange}
|
||||||
onClearHistory={handleClearHistory}
|
onClearHistory={handleClearHistory}
|
||||||
getHistoryLength={getHistoryLength}
|
getHistoryLength={getHistoryLength}
|
||||||
|
hasDraft={!!draft}
|
||||||
|
onResetDraft={handleClear}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Lightweight progress tracker under header */}
|
{/* Lightweight progress tracker under header */}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { LinkedInPreferences } from '../utils/storageUtils';
|
import { LinkedInPreferences } from '../utils/storageUtils';
|
||||||
import { PersonaChip } from '../../TextEditor/ContentPreviewHeaderComponents';
|
import { PersonaChip } from '../../TextEditor/ContentPreviewHeaderComponents';
|
||||||
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
|
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
|
||||||
|
import HeaderControls from '../../shared/HeaderControls';
|
||||||
import BrainstormFlow from './BrainstormFlow';
|
import BrainstormFlow from './BrainstormFlow';
|
||||||
// Temporary fix: use require for image import
|
// Temporary fix: use require for image import
|
||||||
const alwrityLogo = require('../../../assets/images/alwrity_logo.png');
|
const alwrityLogo = require('../../../assets/images/alwrity_logo.png');
|
||||||
@@ -15,6 +16,8 @@ interface HeaderProps {
|
|||||||
onPreferencesChange: (prefs: Partial<LinkedInPreferences>) => void;
|
onPreferencesChange: (prefs: Partial<LinkedInPreferences>) => void;
|
||||||
onClearHistory: () => void;
|
onClearHistory: () => void;
|
||||||
getHistoryLength: () => number;
|
getHistoryLength: () => number;
|
||||||
|
hasDraft: boolean;
|
||||||
|
onResetDraft: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({
|
export const Header: React.FC<HeaderProps> = ({
|
||||||
@@ -24,7 +27,9 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
onPreferencesModalChange,
|
onPreferencesModalChange,
|
||||||
onPreferencesChange,
|
onPreferencesChange,
|
||||||
onClearHistory,
|
onClearHistory,
|
||||||
getHistoryLength
|
getHistoryLength,
|
||||||
|
hasDraft,
|
||||||
|
onResetDraft
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [personaOverride, setPersonaOverride] = useState<any>(null);
|
const [personaOverride, setPersonaOverride] = useState<any>(null);
|
||||||
@@ -91,9 +96,9 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
{/* Left Section - Logo and Title */}
|
{/* Left Section - Logo and Title */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
||||||
{/* Back Button */}
|
{/* Back Button - returns to LinkedIn home (WelcomeMessage) when there's a draft */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/dashboard')}
|
onClick={() => hasDraft ? onResetDraft() : navigate('/')}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
background: 'rgba(255, 255, 255, 0.1)',
|
background: 'rgba(255, 255, 255, 0.1)',
|
||||||
@@ -114,9 +119,9 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
|
||||||
}}
|
}}
|
||||||
title="Back to Dashboard"
|
title={hasDraft ? 'Back to LinkedIn Home' : 'Back to Home'}
|
||||||
>
|
>
|
||||||
← Back to Dashboard
|
← {hasDraft ? 'Back' : 'Home'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||||
@@ -485,6 +490,9 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
>
|
>
|
||||||
Clear Memory ({getHistoryLength()})
|
Clear Memory ({getHistoryLength()})
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Shared Header Controls - Usage Stats & User Dropdown */}
|
||||||
|
<HeaderControls colorMode="light" showAlerts={true} showUser={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,19 @@ export function useLinkedInWriter() {
|
|||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Simulate progress advancement during API call
|
||||||
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
||||||
|
let stepIndex = 0;
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
if (stepIndex < progressStepIds.length) {
|
||||||
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
|
}));
|
||||||
|
stepIndex++;
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await linkedInWriterApi.generatePost({
|
const res = await linkedInWriterApi.generatePost({
|
||||||
topic: params?.topic || prefs.topic || 'AI transformation in business',
|
topic: params?.topic || prefs.topic || 'AI transformation in business',
|
||||||
@@ -115,14 +128,15 @@ export function useLinkedInWriter() {
|
|||||||
grounding_level: 'enhanced' as GroundingLevel,
|
grounding_level: 'enhanced' as GroundingLevel,
|
||||||
include_citations: true
|
include_citations: true
|
||||||
});
|
});
|
||||||
|
clearInterval(progressInterval);
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
|
// Catch up remaining steps
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'prepare_queries', status: 'completed', message: `Prepared ${(res.data?.search_queries || []).length} research queries` } }));
|
while (stepIndex < progressStepIds.length) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'research', status: 'completed', message: `Research completed with ${(res.research_sources || []).length} sources` } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'grounding', status: 'completed', message: 'AI grounding applied successfully' } }));
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'content_generation', status: 'completed', message: 'Content generated' } }));
|
}));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'citations', status: 'completed', message: `Extracted ${(res.data?.citations || []).length} citations` } }));
|
stepIndex++;
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'quality_analysis', status: 'completed', message: 'Quality assessment completed' } }));
|
}
|
||||||
const content = res.data.content;
|
const content = res.data.content;
|
||||||
const hashtags = res.data.hashtags?.map((h: any) => h.hashtag).join(' ') || '';
|
const hashtags = res.data.hashtags?.map((h: any) => h.hashtag).join(' ') || '';
|
||||||
const cta = res.data.call_to_action || '';
|
const cta = res.data.call_to_action || '';
|
||||||
@@ -147,6 +161,7 @@ export function useLinkedInWriter() {
|
|||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||||
return { success: false, error: res.error || 'Generation failed' };
|
return { success: false, error: res.error || 'Generation failed' };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
||||||
return { success: false, error: error.message || 'Generation failed' };
|
return { success: false, error: error.message || 'Generation failed' };
|
||||||
@@ -173,6 +188,18 @@ export function useLinkedInWriter() {
|
|||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
||||||
|
let stepIndex = 0;
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
if (stepIndex < progressStepIds.length) {
|
||||||
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
|
}));
|
||||||
|
stepIndex++;
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await linkedInWriterApi.generateArticle({
|
const res = await linkedInWriterApi.generateArticle({
|
||||||
topic: params?.topic || prefs.topic || 'Digital transformation strategies',
|
topic: params?.topic || prefs.topic || 'Digital transformation strategies',
|
||||||
@@ -188,14 +215,14 @@ export function useLinkedInWriter() {
|
|||||||
grounding_level: 'enhanced' as GroundingLevel,
|
grounding_level: 'enhanced' as GroundingLevel,
|
||||||
include_citations: true
|
include_citations: true
|
||||||
});
|
});
|
||||||
|
clearInterval(progressInterval);
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
|
while (stepIndex < progressStepIds.length) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'prepare_queries', status: 'completed', message: `Prepared ${(res.data?.search_queries || []).length} research queries` } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'research', status: 'completed', message: `Research completed with ${(res.research_sources || []).length} sources` } }));
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'grounding', status: 'completed', message: 'AI grounding applied successfully' } }));
|
}));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'content_generation', status: 'completed', message: 'Article content generated' } }));
|
stepIndex++;
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'citations', status: 'completed', message: `Extracted ${(res.data?.citations || []).length} citations` } }));
|
}
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'quality_analysis', status: 'completed', message: 'Quality assessment completed' } }));
|
|
||||||
const content = `# ${res.data.title}\n\n${res.data.content}`;
|
const content = `# ${res.data.title}\n\n${res.data.content}`;
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:updateGroundingData', { detail: {
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateGroundingData', { detail: {
|
||||||
researchSources: res.research_sources || [],
|
researchSources: res.research_sources || [],
|
||||||
@@ -211,10 +238,12 @@ export function useLinkedInWriter() {
|
|||||||
trackActionUsage('generateLinkedInArticle');
|
trackActionUsage('generateLinkedInArticle');
|
||||||
return { success: true, data: res.data };
|
return { success: true, data: res.data };
|
||||||
}
|
}
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||||
return { success: false, error: res.error || 'Generation failed' };
|
return { success: false, error: res.error || 'Generation failed' };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
||||||
return { success: false, error: error.message || 'Generation failed' };
|
return { success: false, error: error.message || 'Generation failed' };
|
||||||
@@ -241,6 +270,18 @@ export function useLinkedInWriter() {
|
|||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
||||||
|
let stepIndex = 0;
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
if (stepIndex < progressStepIds.length) {
|
||||||
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
|
}));
|
||||||
|
stepIndex++;
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await linkedInWriterApi.generateCarousel({
|
const res = await linkedInWriterApi.generateCarousel({
|
||||||
topic: params?.topic || prefs.topic || 'Professional development tips',
|
topic: params?.topic || prefs.topic || 'Professional development tips',
|
||||||
@@ -253,14 +294,14 @@ export function useLinkedInWriter() {
|
|||||||
include_cta_slide: params?.include_cta_slide ?? (prefs.include_cta_slide ?? true),
|
include_cta_slide: params?.include_cta_slide ?? (prefs.include_cta_slide ?? true),
|
||||||
visual_style: params?.visual_style || prefs.visual_style || 'modern'
|
visual_style: params?.visual_style || prefs.visual_style || 'modern'
|
||||||
});
|
});
|
||||||
|
clearInterval(progressInterval);
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
|
while (stepIndex < progressStepIds.length) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'prepare_queries', status: 'completed', message: 'Prepared research queries' } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'research', status: 'completed', message: 'Research completed' } }));
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'grounding', status: 'completed', message: 'AI grounding applied' } }));
|
}));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'content_generation', status: 'completed', message: `Generated ${res.data.slides?.length || 0} slides` } }));
|
stepIndex++;
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'citations', status: 'completed', message: 'Citations extracted' } }));
|
}
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'quality_analysis', status: 'completed', message: 'Quality assessment completed' } }));
|
|
||||||
let content = `# ${res.data.title}\n\n`;
|
let content = `# ${res.data.title}\n\n`;
|
||||||
res.data.slides.forEach((slide: any, index: number) => {
|
res.data.slides.forEach((slide: any, index: number) => {
|
||||||
content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
|
content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
|
||||||
@@ -272,10 +313,12 @@ export function useLinkedInWriter() {
|
|||||||
trackActionUsage('generateLinkedInCarousel');
|
trackActionUsage('generateLinkedInCarousel');
|
||||||
return { success: true, data: res.data };
|
return { success: true, data: res.data };
|
||||||
}
|
}
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||||
return { success: false, error: res.error || 'Generation failed' };
|
return { success: false, error: res.error || 'Generation failed' };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
||||||
return { success: false, error: error.message || 'Generation failed' };
|
return { success: false, error: error.message || 'Generation failed' };
|
||||||
@@ -302,6 +345,18 @@ export function useLinkedInWriter() {
|
|||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
||||||
|
let stepIndex = 0;
|
||||||
|
const progressInterval = setInterval(() => {
|
||||||
|
if (stepIndex < progressStepIds.length) {
|
||||||
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
|
}));
|
||||||
|
stepIndex++;
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await linkedInWriterApi.generateVideoScript({
|
const res = await linkedInWriterApi.generateVideoScript({
|
||||||
topic: params?.topic || prefs.topic || 'Professional networking tips',
|
topic: params?.topic || prefs.topic || 'Professional networking tips',
|
||||||
@@ -313,14 +368,14 @@ export function useLinkedInWriter() {
|
|||||||
include_hook: params?.include_hook ?? (prefs.include_hook ?? true),
|
include_hook: params?.include_hook ?? (prefs.include_hook ?? true),
|
||||||
include_captions: params?.include_captions ?? (prefs.include_captions ?? true)
|
include_captions: params?.include_captions ?? (prefs.include_captions ?? true)
|
||||||
});
|
});
|
||||||
|
clearInterval(progressInterval);
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
|
while (stepIndex < progressStepIds.length) {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'prepare_queries', status: 'completed', message: 'Prepared research queries' } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'research', status: 'completed', message: 'Research completed' } }));
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'grounding', status: 'completed', message: 'AI grounding applied' } }));
|
}));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'content_generation', status: 'completed', message: `Generated script with ${res.data.main_content?.length || 0} scenes` } }));
|
stepIndex++;
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'citations', status: 'completed', message: 'Citations extracted' } }));
|
}
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'quality_analysis', status: 'completed', message: 'Quality assessment completed' } }));
|
|
||||||
let content = `# Video Script: ${params?.topic || 'Professional Content'}\n\n`;
|
let content = `# Video Script: ${params?.topic || 'Professional Content'}\n\n`;
|
||||||
content += `## Hook\n${res.data.hook}\n\n`;
|
content += `## Hook\n${res.data.hook}\n\n`;
|
||||||
content += `## Main Content\n`;
|
content += `## Main Content\n`;
|
||||||
@@ -339,10 +394,12 @@ export function useLinkedInWriter() {
|
|||||||
trackActionUsage('generateLinkedInVideoScript');
|
trackActionUsage('generateLinkedInVideoScript');
|
||||||
return { success: true, data: res.data };
|
return { success: true, data: res.data };
|
||||||
}
|
}
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
||||||
return { success: false, error: res.error || 'Generation failed' };
|
return { success: false, error: res.error || 'Generation failed' };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
clearInterval(progressInterval);
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
||||||
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
||||||
return { success: false, error: error.message || 'Generation failed' };
|
return { success: false, error: error.message || 'Generation failed' };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PersonaEditorModal from './PersonaEditorModal';
|
import PersonaEditorModal from './PersonaEditorModal';
|
||||||
import { getUserPersonas, getPlatformPersona, updatePersona, updatePlatformPersona } from '../../../api/persona';
|
import { getUserPersonas, getPlatformPersona, updatePersona, updatePlatformPersona } from '../../../api/persona';
|
||||||
|
import { shouldSkipOnboarding } from '../../../utils/demoMode';
|
||||||
|
|
||||||
interface PersonaData {
|
interface PersonaData {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -43,31 +44,38 @@ const PersonaChip: React.FC<PersonaChipProps> = ({
|
|||||||
|
|
||||||
// Fetch persona data
|
// Fetch persona data
|
||||||
const fetchPersonaData = async () => {
|
const fetchPersonaData = async () => {
|
||||||
|
// Skip API calls in feature-only mode (no persona data available)
|
||||||
|
if (shouldSkipOnboarding()) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch core persona list (take most recent active) and platform-specific details using authenticated API client
|
|
||||||
const [coreList, platformData] = await Promise.all([
|
const [coreList, platformData] = await Promise.all([
|
||||||
getUserPersonas(),
|
getUserPersonas(),
|
||||||
getPlatformPersona(platform)
|
getPlatformPersona(platform)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!coreList || !platformData) {
|
||||||
|
setPersonaData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (coreList && platformData) {
|
if (coreList && platformData) {
|
||||||
// Extract core persona from the response
|
|
||||||
const corePersona = platformData?.core_persona || {};
|
const corePersona = platformData?.core_persona || {};
|
||||||
const platformPersona = platformData?.platform_persona || {};
|
const platformPersona = platformData?.platform_persona || {};
|
||||||
const qualityMetrics = platformData?.quality_metrics || {};
|
const qualityMetrics = platformData?.quality_metrics || {};
|
||||||
|
|
||||||
if (!corePersona || Object.keys(corePersona).length === 0) {
|
if (!corePersona || Object.keys(corePersona).length === 0) {
|
||||||
setError('No persona found for this platform');
|
setPersonaData(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge core + platform fields for editor convenience
|
|
||||||
setPersonaData({
|
setPersonaData({
|
||||||
id: platformData?.id || 1,
|
id: platformData?.id || 1,
|
||||||
user_id: 1, // Placeholder, not used
|
user_id: 1,
|
||||||
persona_name: corePersona.persona_name || 'Untitled Persona',
|
persona_name: corePersona.persona_name || 'Untitled Persona',
|
||||||
archetype: corePersona.archetype || 'General',
|
archetype: corePersona.archetype || 'General',
|
||||||
core_belief: corePersona.core_belief || '',
|
core_belief: corePersona.core_belief || '',
|
||||||
@@ -212,7 +220,7 @@ const PersonaChip: React.FC<PersonaChipProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error || !personaData) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
background: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)',
|
background: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)',
|
||||||
@@ -241,6 +249,35 @@ const PersonaChip: React.FC<PersonaChipProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!personaData) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%)',
|
||||||
|
border: '1px solid #9ca3af',
|
||||||
|
borderRadius: '999px',
|
||||||
|
padding: '6px 14px',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#6b7280',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => fetchPersonaData()}
|
||||||
|
title="No persona configured yet. Click to retry."
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
width: '8px',
|
||||||
|
height: '8px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: '#9ca3af'
|
||||||
|
}} />
|
||||||
|
No Persona
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const confidence = personaData.confidence_score || 0;
|
const confidence = personaData.confidence_score || 0;
|
||||||
const confidenceColor = getPersonaColor(confidence);
|
const confidenceColor = getPersonaColor(confidence);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user