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 "") 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),

View File

@@ -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');
} }

View File

@@ -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');

View File

@@ -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 */}

View File

@@ -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>

View File

@@ -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' };

View File

@@ -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);