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:
ajaysi
2026-06-13 17:12:45 +05:30
parent d90d441019
commit ce9bf293ed
7 changed files with 170 additions and 45 deletions

View File

@@ -478,8 +478,25 @@ class ContentGenerator:
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 {
'content': content_text,
'title': title,
'sources': [],
'citations': [],
'grounding_enabled': bool(research_sources),

View File

@@ -121,11 +121,12 @@ export const generateWritingPersona = async (userId: number = 1, request: Person
* Get all writing personas for a user
* 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 {
const response = await apiClient.get('/api/personas/user');
return response.data;
} catch (error: any) {
if (error.response?.status === 404) return null;
console.error('Error getting user personas:', error);
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}`);
return response.data;
} catch (error: any) {
if (error.response?.status === 404) return null;
console.error('Error getting platform persona:', error);
throw new Error(error.response?.data?.detail || 'Failed to get platform persona');
}

View File

@@ -145,12 +145,14 @@ const InitialRouteHandler: React.FC = () => {
return navigateAndLog(redirectTo);
}
// Feature-only mode (e.g., ALWRITY_ENABLED_FEATURES=linkedin)
if (shouldSkipOnboarding()) {
const route = getDefaultLandingRoute();
console.log(`InitialRouteHandler: Checkout success in demo mode → ${route}`);
console.log(`InitialRouteHandler: Checkout success — feature-only mode → ${route}`);
return navigateAndLog(route);
}
// Full mode: check if onboarding is needed
if (!isOnboardingComplete) {
console.log('InitialRouteHandler: Checkout success — onboarding incomplete → Onboarding');
return navigateAndLog('/onboarding');
@@ -299,9 +301,9 @@ const InitialRouteHandler: React.FC = () => {
}
if (shouldSkipOnboarding()) {
const route = getDefaultLandingRoute();
console.log(`InitialRouteHandler: Demo mode - no subscription but allowing access to ${route}`);
return navigateAndLog(route);
// Feature-only mode still requires subscription
console.log('InitialRouteHandler: No subscription data in feature-only mode → Pricing page');
return navigateAndLog("/pricing");
}
console.log('InitialRouteHandler: No subscription data after check → Pricing page');

View File

@@ -91,7 +91,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
// Handlers
handleDraftChange,
handleContextChange,
// handleClear,
handleClear,
// handleCopy,
handleClearHistory,
@@ -438,6 +438,8 @@ Always use the most appropriate tool for the user's request.`.trim();
onPreferencesChange={handlePreferencesChange}
onClearHistory={handleClearHistory}
getHistoryLength={getHistoryLength}
hasDraft={!!draft}
onResetDraft={handleClear}
/>
{/* Lightweight progress tracker under header */}

View File

@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { LinkedInPreferences } from '../utils/storageUtils';
import { PersonaChip } from '../../TextEditor/ContentPreviewHeaderComponents';
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
import HeaderControls from '../../shared/HeaderControls';
import BrainstormFlow from './BrainstormFlow';
// Temporary fix: use require for image import
const alwrityLogo = require('../../../assets/images/alwrity_logo.png');
@@ -15,6 +16,8 @@ interface HeaderProps {
onPreferencesChange: (prefs: Partial<LinkedInPreferences>) => void;
onClearHistory: () => void;
getHistoryLength: () => number;
hasDraft: boolean;
onResetDraft: () => void;
}
export const Header: React.FC<HeaderProps> = ({
@@ -24,7 +27,9 @@ export const Header: React.FC<HeaderProps> = ({
onPreferencesModalChange,
onPreferencesChange,
onClearHistory,
getHistoryLength
getHistoryLength,
hasDraft,
onResetDraft
}) => {
const navigate = useNavigate();
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' }}>
{/* Left Section - Logo and Title */}
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
{/* Back Button */}
{/* Back Button - returns to LinkedIn home (WelcomeMessage) when there's a draft */}
<button
onClick={() => navigate('/dashboard')}
onClick={() => hasDraft ? onResetDraft() : navigate('/')}
style={{
padding: '8px 12px',
background: 'rgba(255, 255, 255, 0.1)',
@@ -114,9 +119,9 @@ export const Header: React.FC<HeaderProps> = ({
onMouseLeave={(e) => {
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>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
@@ -485,6 +490,9 @@ export const Header: React.FC<HeaderProps> = ({
>
Clear Memory ({getHistoryLength()})
</button>
{/* Shared Header Controls - Usage Stats & User Dropdown */}
<HeaderControls colorMode="light" showAlerts={true} showUser={true} />
</div>
</div>

View File

@@ -99,6 +99,19 @@ export function useLinkedInWriter() {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
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 {
const res = await linkedInWriterApi.generatePost({
topic: params?.topic || prefs.topic || 'AI transformation in business',
@@ -115,14 +128,15 @@ export function useLinkedInWriter() {
grounding_level: 'enhanced' as GroundingLevel,
include_citations: true
});
clearInterval(progressInterval);
if (res.success && res.data) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
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', { detail: { id: 'research', status: 'completed', message: `Research completed with ${(res.research_sources || []).length} sources` } }));
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: 'Content generated' } }));
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' } }));
// Catch up remaining steps
while (stepIndex < progressStepIds.length) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
detail: { id: progressStepIds[stepIndex], status: 'completed' }
}));
stepIndex++;
}
const content = res.data.content;
const hashtags = res.data.hashtags?.map((h: any) => h.hashtag).join(' ') || '';
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 } }));
return { success: false, error: res.error || 'Generation failed' };
} catch (error: any) {
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
return { success: false, error: error.message || 'Generation failed' };
@@ -173,6 +188,18 @@ export function useLinkedInWriter() {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
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 {
const res = await linkedInWriterApi.generateArticle({
topic: params?.topic || prefs.topic || 'Digital transformation strategies',
@@ -188,14 +215,14 @@ export function useLinkedInWriter() {
grounding_level: 'enhanced' as GroundingLevel,
include_citations: true
});
clearInterval(progressInterval);
if (res.success && res.data) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
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', { detail: { id: 'research', status: 'completed', message: `Research completed with ${(res.research_sources || []).length} sources` } }));
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' } }));
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' } }));
while (stepIndex < progressStepIds.length) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
detail: { id: progressStepIds[stepIndex], status: 'completed' }
}));
stepIndex++;
}
const content = `# ${res.data.title}\n\n${res.data.content}`;
window.dispatchEvent(new CustomEvent('linkedinwriter:updateGroundingData', { detail: {
researchSources: res.research_sources || [],
@@ -211,10 +238,12 @@ export function useLinkedInWriter() {
trackActionUsage('generateLinkedInArticle');
return { success: true, data: res.data };
}
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
return { success: false, error: res.error || 'Generation failed' };
} catch (error: any) {
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
return { success: false, error: error.message || 'Generation failed' };
@@ -241,6 +270,18 @@ export function useLinkedInWriter() {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
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 {
const res = await linkedInWriterApi.generateCarousel({
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),
visual_style: params?.visual_style || prefs.visual_style || 'modern'
});
clearInterval(progressInterval);
if (res.success && res.data) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'prepare_queries', status: 'completed', message: 'Prepared research queries' } }));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'research', status: 'completed', message: 'Research 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` } }));
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' } }));
while (stepIndex < progressStepIds.length) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
detail: { id: progressStepIds[stepIndex], status: 'completed' }
}));
stepIndex++;
}
let content = `# ${res.data.title}\n\n`;
res.data.slides.forEach((slide: any, index: number) => {
content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
@@ -272,10 +313,12 @@ export function useLinkedInWriter() {
trackActionUsage('generateLinkedInCarousel');
return { success: true, data: res.data };
}
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
return { success: false, error: res.error || 'Generation failed' };
} catch (error: any) {
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
return { success: false, error: error.message || 'Generation failed' };
@@ -302,6 +345,18 @@ export function useLinkedInWriter() {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
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 {
const res = await linkedInWriterApi.generateVideoScript({
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_captions: params?.include_captions ?? (prefs.include_captions ?? true)
});
clearInterval(progressInterval);
if (res.success && res.data) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'personalize', status: 'completed', message: 'Topic personalized successfully' } }));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'prepare_queries', status: 'completed', message: 'Prepared research queries' } }));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'research', status: 'completed', message: 'Research 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` } }));
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' } }));
while (stepIndex < progressStepIds.length) {
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
detail: { id: progressStepIds[stepIndex], status: 'completed' }
}));
stepIndex++;
}
let content = `# Video Script: ${params?.topic || 'Professional Content'}\n\n`;
content += `## Hook\n${res.data.hook}\n\n`;
content += `## Main Content\n`;
@@ -339,10 +394,12 @@ export function useLinkedInWriter() {
trackActionUsage('generateLinkedInVideoScript');
return { success: true, data: res.data };
}
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
return { success: false, error: res.error || 'Generation failed' };
} catch (error: any) {
clearInterval(progressInterval);
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
return { success: false, error: error.message || 'Generation failed' };

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import PersonaEditorModal from './PersonaEditorModal';
import { getUserPersonas, getPlatformPersona, updatePersona, updatePlatformPersona } from '../../../api/persona';
import { shouldSkipOnboarding } from '../../../utils/demoMode';
interface PersonaData {
id?: number;
@@ -43,31 +44,38 @@ const PersonaChip: React.FC<PersonaChipProps> = ({
// Fetch persona data
const fetchPersonaData = async () => {
// Skip API calls in feature-only mode (no persona data available)
if (shouldSkipOnboarding()) {
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
try {
// Fetch core persona list (take most recent active) and platform-specific details using authenticated API client
const [coreList, platformData] = await Promise.all([
getUserPersonas(),
getPlatformPersona(platform)
]);
if (!coreList || !platformData) {
setPersonaData(null);
return;
}
if (coreList && platformData) {
// Extract core persona from the response
const corePersona = platformData?.core_persona || {};
const platformPersona = platformData?.platform_persona || {};
const qualityMetrics = platformData?.quality_metrics || {};
if (!corePersona || Object.keys(corePersona).length === 0) {
setError('No persona found for this platform');
setPersonaData(null);
return;
}
// Merge core + platform fields for editor convenience
setPersonaData({
id: platformData?.id || 1,
user_id: 1, // Placeholder, not used
user_id: 1,
persona_name: corePersona.persona_name || 'Untitled Persona',
archetype: corePersona.archetype || 'General',
core_belief: corePersona.core_belief || '',
@@ -212,7 +220,7 @@ const PersonaChip: React.FC<PersonaChipProps> = ({
);
}
if (error || !personaData) {
if (error) {
return (
<div style={{
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 confidenceColor = getPersonaColor(confidence);