diff --git a/backend/services/linkedin/content_generator.py b/backend/services/linkedin/content_generator.py index 3f598dfd..44d30e18 100644 --- a/backend/services/linkedin/content_generator.py +++ b/backend/services/linkedin/content_generator.py @@ -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), diff --git a/frontend/src/api/persona.ts b/frontend/src/api/persona.ts index ec5625b8..a6775b64 100644 --- a/frontend/src/api/persona.ts +++ b/frontend/src/api/persona.ts @@ -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 => { 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'); } diff --git a/frontend/src/components/App/InitialRouteHandler.tsx b/frontend/src/components/App/InitialRouteHandler.tsx index f9d70857..5d217afa 100644 --- a/frontend/src/components/App/InitialRouteHandler.tsx +++ b/frontend/src/components/App/InitialRouteHandler.tsx @@ -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'); diff --git a/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx b/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx index 1ca909aa..a9f44aea 100644 --- a/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx +++ b/frontend/src/components/LinkedInWriter/LinkedInWriter.tsx @@ -91,7 +91,7 @@ const LinkedInWriterContent: React.FC = ({ 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 */} diff --git a/frontend/src/components/LinkedInWriter/components/Header.tsx b/frontend/src/components/LinkedInWriter/components/Header.tsx index 6370d09d..8ef2800b 100644 --- a/frontend/src/components/LinkedInWriter/components/Header.tsx +++ b/frontend/src/components/LinkedInWriter/components/Header.tsx @@ -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) => void; onClearHistory: () => void; getHistoryLength: () => number; + hasDraft: boolean; + onResetDraft: () => void; } export const Header: React.FC = ({ @@ -24,7 +27,9 @@ export const Header: React.FC = ({ onPreferencesModalChange, onPreferencesChange, onClearHistory, - getHistoryLength + getHistoryLength, + hasDraft, + onResetDraft }) => { const navigate = useNavigate(); const [personaOverride, setPersonaOverride] = useState(null); @@ -91,9 +96,9 @@ export const Header: React.FC = ({
{/* Left Section - Logo and Title */}
- {/* Back Button */} + {/* Back Button - returns to LinkedIn home (WelcomeMessage) when there's a draft */}
@@ -485,6 +490,9 @@ export const Header: React.FC = ({ > Clear Memory ({getHistoryLength()}) + + {/* Shared Header Controls - Usage Stats & User Dropdown */} +
diff --git a/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts b/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts index 59c71482..419bb99a 100644 --- a/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts +++ b/frontend/src/components/LinkedInWriter/hooks/useLinkedInWriter.ts @@ -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' }; diff --git a/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx b/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx index 134c8490..4e482401 100644 --- a/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx +++ b/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx @@ -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 = ({ // 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 = ({ ); } - if (error || !personaData) { + if (error) { return (
= ({ ); } + if (!personaData) { + return ( +
fetchPersonaData()} + title="No persona configured yet. Click to retry." + > +
+ No Persona +
+ ); + } + const confidence = personaData.confidence_score || 0; const confidenceColor = getPersonaColor(confidence);