import { useEffect, useMemo, useState } from 'react' import { PenSquare, RotateCcw } from 'lucide-react' import { Button, Card, Loader } from '../../components/common' import { endpoints, getErrorMessage } from '../../api/client' import { t, tf } from '../../lib/i18n' import type { ContentPlan, PostStructure, Reporter, SavedContentPlan } from '../../types' import type { GenerationConfig } from '../../components/generator' import { ConversationPhaseStep } from './ConversationPhaseStep' import { createEmptyContentPlan } from './conversation-utils' interface ContentStepProps { config: GenerationConfig reporter: Reporter | null technicalSummary: string technicalAnswers: Record[] savedPlan: SavedContentPlan | null forceEdit: boolean sessionId: string | null initialState: Record onSessionChange: (sessionId: string) => void onStateChange: (state: Record) => void onBack: () => void onReuseSavedPlan: () => void onEditSavedPlan: () => void onStartFresh: () => void onContinue: (state: Record) => void onSkip: () => void } function planFieldValue(values: string[] | undefined): string { return values && values.length > 0 ? values.join(', ') : '—' } function planPreviewRows(plan: ContentPlan) { return [ { label: t('Audience'), value: plan.audience.primary || '—' }, { label: t('Goal'), value: plan.goal.primary || '—' }, { label: t('Focus Areas'), value: planFieldValue(plan.focus_areas) }, { label: t('Standards'), value: planFieldValue(plan.standards_to_reference) }, { label: t('Must Include'), value: planFieldValue(plan.must_include) }, { label: t('Avoid'), value: planFieldValue(plan.avoid) }, { label: t('CTA'), value: plan.cta_style || plan.goal.cta || '—' }, ] } function summarizeResearchScope(plan: ContentPlan): string { if (!plan.research_enabled) { return t('Disabled') } const parts = [ plan.research_scope.directive, plan.research_scope.languages.length > 0 ? tf('Languages: %s', plan.research_scope.languages.join(', ')) : '', plan.research_scope.regions.length > 0 ? tf('Regions: %s', plan.research_scope.regions.join(', ')) : '', plan.research_scope.source_preferences.length > 0 ? tf('Sources: %s', plan.research_scope.source_preferences.join(', ')) : '', plan.research_scope.freshness ? tf('Freshness: %s', plan.research_scope.freshness) : '', ].filter(Boolean) return parts.length > 0 ? parts.join(' • ') : t('Enabled') } function getTopicTreePillars(plan: ContentPlan | null | undefined) { return plan?.topic_tree?.pillars ?? [] } function countTopicTreeAngles(plan: ContentPlan): number { const pillars = getTopicTreePillars(plan) const articleCount = pillars.reduce((total, pillar) => ( total + (pillar.clusters || []).reduce((clusterTotal, cluster) => clusterTotal + (cluster.articles || []).length, 0) ), 0) if (articleCount > 0) return articleCount const fromCounts = pillars.reduce((total, pillar) => ( total + (pillar.article_count || (pillar.clusters || []).reduce((ct, c) => ct + (c.article_count || 0), 0)) ), 0) if (fromCounts > 0) return fromCounts return plan.topic_tree?.total_articles || 0 } export function ContentStep({ config, reporter, technicalSummary, technicalAnswers, savedPlan, forceEdit, sessionId, initialState, onSessionChange, onStateChange, onBack, onReuseSavedPlan, onEditSavedPlan, onStartFresh, onContinue, onSkip, }: ContentStepProps) { const [structure, setStructure] = useState(null) const [loading, setLoading] = useState(true) const [loadError, setLoadError] = useState(null) useEffect(() => { let cancelled = false void (async () => { setLoading(true) setLoadError(null) try { const res = await endpoints.getStructure(config.postType) if (!cancelled) { setStructure(res.data as PostStructure) } } catch (error) { if (!cancelled) { setLoadError(getErrorMessage(error, t('Unknown error'))) } } finally { if (!cancelled) { setLoading(false) } } })() return () => { cancelled = true } }, [config.postType]) const context = useMemo(() => { return { post_type: structure ? { slug: structure.post_type, label: structure.post_type_label, meta_fields: structure.meta_fields || [], } : null, topic: config.topic, count: config.count, length: config.length, generation_type: config.generationType, reporter, technical_summary: technicalSummary, technical_answers: technicalAnswers, saved_content_plan: savedPlan ? { summary: savedPlan.summary, content_plan: savedPlan.content_plan, still_valid: savedPlan.still_valid, } : null, } }, [config.count, config.generationType, config.length, config.topic, reporter, savedPlan, structure, technicalAnswers, technicalSummary]) if (loading) { return (
) } if (loadError) { return (

{t('Error loading content types')}

{loadError}

) } if (savedPlan?.still_valid && !forceEdit && !sessionId) { return (

{t('Content Planning')}

{t('Content Plan')}

{planPreviewRows(savedPlan.content_plan || createEmptyContentPlan()).map((row) => (
{row.label}: {row.value}
))}
{t('Research')}: {summarizeResearchScope(savedPlan.content_plan || createEmptyContentPlan())}
{t('Topic Tree')}:{' '} {countTopicTreeAngles(savedPlan.content_plan || createEmptyContentPlan()) > 0 ? tf('%d angles across %d pillars', countTopicTreeAngles(savedPlan.content_plan || createEmptyContentPlan()), savedPlan.content_plan?.topic_tree?.pillars?.length || 0) : t('Not planned yet')}
{t('Summary')}: {savedPlan.summary || '—'}
) } return ( { const plan = ((draftState.content_plan as ContentPlan | undefined) || createEmptyContentPlan()) const topicTreePillars = getTopicTreePillars(plan) return (
{t('Content Plan')}
{planPreviewRows(plan).map((row) => (
{row.label}: {row.value}
))}
{t('Summary')}: {plan.summary || '—'}
{t('Research')}: {summarizeResearchScope(plan)}
{t('Topic Tree')}:{' '} {countTopicTreeAngles(plan) > 0 ? tf('%d angles across %d pillars', countTopicTreeAngles(plan), topicTreePillars.length) : t('Not planned yet')}
{topicTreePillars.length > 0 ? (
{topicTreePillars.map((pillar) => (
{pillar.title || (pillar as any).name || (pillar as any).topic || t('Untitled pillar')}
{(pillar.clusters || []).map((cluster) => (
{cluster.title || (cluster as any).name || (cluster as any).topic || t('Untitled cluster')}
{(cluster.articles || []).map((article) => ( {article.angle} ))}
))}
))}
) : null}
) }} /> ) }