ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
21
lib/content_calendar/ui/add_content_modal.py
Normal file
21
lib/content_calendar/ui/add_content_modal.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import streamlit as st
|
||||
|
||||
def render_add_content_modal(selected_date, on_add_content, on_generate_with_ai):
|
||||
if st.button("+ Add Content", key="open_add_content_dialog_bottom"):
|
||||
st.session_state['show_add_content_dialog'] = True
|
||||
if st.session_state.get('show_add_content_dialog', False):
|
||||
st.markdown("### Add Content")
|
||||
with st.form("quick_add_form_dialog_bottom"):
|
||||
title = st.text_input("Title")
|
||||
platform = st.selectbox("Platform", ["Blog", "Instagram", "Twitter", "LinkedIn", "Facebook"])
|
||||
content_type = st.selectbox("Content Type", ["Article", "Social Post", "Video", "Newsletter"])
|
||||
publish_date = st.date_input("Publish Date", selected_date)
|
||||
col_add, col_ai = st.columns([0.6, 0.4])
|
||||
with col_add:
|
||||
if st.form_submit_button("Add Content"):
|
||||
on_add_content(title, platform, content_type, publish_date)
|
||||
with col_ai:
|
||||
if st.form_submit_button("Generate with AI"):
|
||||
on_generate_with_ai(title, platform, content_type)
|
||||
if st.button("Close", key="close_add_content_dialog_bottom"):
|
||||
st.session_state['show_add_content_dialog'] = False
|
||||
137
lib/content_calendar/ui/ai_suggestions_modal.py
Normal file
137
lib/content_calendar/ui/ai_suggestions_modal.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import streamlit as st
|
||||
|
||||
def render_ai_suggestions_modal(generate_ai_suggestions, on_create_brief, on_schedule, on_refine, on_customize):
|
||||
st.subheader("AI Content Suggestions")
|
||||
default_type = st.session_state.get('ai_modal_type', "Blog Post")
|
||||
default_topic = st.session_state.get('ai_modal_topic', "")
|
||||
default_platform = st.session_state.get('ai_modal_platform', "Blog")
|
||||
content_types = {
|
||||
"Blog Post": "Long-form content for in-depth topics",
|
||||
"Social Media Post": "Short, engaging content for social platforms",
|
||||
"Video": "Visual content with script and storyboard",
|
||||
"Newsletter": "Email content for subscriber engagement"
|
||||
}
|
||||
content_type = st.selectbox(
|
||||
"Content Type",
|
||||
list(content_types.keys()),
|
||||
format_func=lambda x: f"{x} - {content_types[x]}",
|
||||
key="modal_suggestion_type",
|
||||
index=list(content_types.keys()).index(default_type) if default_type in content_types else 0
|
||||
)
|
||||
topic = st.text_input("Enter topic or keyword", value=default_topic, key="modal_suggestion_topic")
|
||||
with st.expander("Advanced Options"):
|
||||
audience = st.multiselect(
|
||||
"Target Audience",
|
||||
["Professionals", "Students", "Entrepreneurs", "General Public", "Industry Experts"],
|
||||
default=["Professionals"]
|
||||
)
|
||||
goals = st.multiselect(
|
||||
"Content Goals",
|
||||
["Increase Engagement", "Generate Leads", "Build Authority", "Drive Traffic", "Educate"],
|
||||
default=["Increase Engagement"]
|
||||
)
|
||||
tone = st.select_slider(
|
||||
"Content Tone",
|
||||
options=["Professional", "Casual", "Educational", "Entertaining", "Persuasive"],
|
||||
value="Professional"
|
||||
)
|
||||
length = st.radio(
|
||||
"Content Length",
|
||||
["Short", "Medium", "Long"],
|
||||
horizontal=True
|
||||
)
|
||||
st.subheader("AI Model Settings")
|
||||
model_settings = {
|
||||
"Creativity Level": st.slider("Creativity Level", 0.0, 1.0, 0.7, 0.1),
|
||||
"Formality Level": st.slider("Formality Level", 0.0, 1.0, 0.5, 0.1),
|
||||
"Technical Depth": st.slider("Technical Depth", 0.0, 1.0, 0.5, 0.1)
|
||||
}
|
||||
st.subheader("Content Style Preferences")
|
||||
style_preferences = {
|
||||
"Use Examples": st.checkbox("Include Real-world Examples", True),
|
||||
"Use Statistics": st.checkbox("Include Statistics and Data", True),
|
||||
"Use Quotes": st.checkbox("Include Expert Quotes", False),
|
||||
"Use Case Studies": st.checkbox("Include Case Studies", False)
|
||||
}
|
||||
st.subheader("SEO Preferences")
|
||||
seo_preferences = {
|
||||
"Keyword Density": st.slider("Keyword Density (%)", 1, 5, 2),
|
||||
"Internal Linking": st.checkbox("Suggest Internal Links", True),
|
||||
"External Linking": st.checkbox("Suggest External Links", True),
|
||||
"Meta Description": st.checkbox("Generate Meta Description", True)
|
||||
}
|
||||
st.subheader("Platform-specific Settings")
|
||||
platform_settings = {
|
||||
"Hashtag Usage": st.checkbox("Suggest Hashtags", True),
|
||||
"Image Suggestions": st.checkbox("Suggest Images", True),
|
||||
"Video Suggestions": st.checkbox("Suggest Videos", False),
|
||||
"Interactive Elements": st.checkbox("Suggest Interactive Elements", False)
|
||||
}
|
||||
if st.button("Generate Suggestions", type="primary", key="modal_generate_btn"):
|
||||
with st.spinner("Generating suggestions..."):
|
||||
suggestions = generate_ai_suggestions(
|
||||
content_type,
|
||||
topic,
|
||||
audience,
|
||||
goals,
|
||||
tone,
|
||||
length,
|
||||
model_settings,
|
||||
style_preferences,
|
||||
seo_preferences,
|
||||
platform_settings
|
||||
)
|
||||
if suggestions:
|
||||
suggestion_tabs = st.tabs([f"Suggestion {i+1}" for i in range(len(suggestions))])
|
||||
for i, (tab, suggestion) in enumerate(zip(suggestion_tabs, suggestions)):
|
||||
with tab:
|
||||
col1, col2 = st.columns([2, 1])
|
||||
with col1:
|
||||
st.subheader(suggestion['title'])
|
||||
st.write(f"**Type:** {suggestion['type']}")
|
||||
st.write(f"**Platform:** {suggestion['platform']}")
|
||||
st.write(f"**Target Audience:** {', '.join(suggestion['audience'])}")
|
||||
st.write(f"**Estimated Impact:** {suggestion['impact']}")
|
||||
with st.expander("Content Preview"):
|
||||
st.write(suggestion.get('preview', 'Preview not available'))
|
||||
if suggestion.get('style_elements'):
|
||||
st.write("**Style Elements:**")
|
||||
for element in suggestion['style_elements']:
|
||||
st.write(f"- {element}")
|
||||
if suggestion.get('seo_elements'):
|
||||
st.write("**SEO Elements:**")
|
||||
for element in suggestion['seo_elements']:
|
||||
st.write(f"- {element}")
|
||||
with col2:
|
||||
st.subheader("Performance Metrics")
|
||||
metrics = {
|
||||
"Engagement Score": suggestion.get('engagement_score', '85%'),
|
||||
"Reach Potential": suggestion.get('reach', 'High'),
|
||||
"Conversion Rate": suggestion.get('conversion', '3.5%'),
|
||||
"SEO Impact": suggestion.get('seo_impact', 'Strong')
|
||||
}
|
||||
for metric, value in metrics.items():
|
||||
st.metric(metric, value)
|
||||
st.subheader("Actions")
|
||||
if st.button("Create Brief", key=f"modal_brief_{i}"):
|
||||
on_create_brief(suggestion)
|
||||
if st.button("Schedule", key=f"modal_schedule_{i}"):
|
||||
on_schedule(suggestion)
|
||||
if st.button("Refine", key=f"modal_refine_{i}"):
|
||||
on_refine(suggestion)
|
||||
if st.button("Customize", key=f"modal_customize_{i}"):
|
||||
on_customize(suggestion)
|
||||
with st.expander("Additional Options"):
|
||||
st.write("**Platform Optimizations**")
|
||||
for platform in suggestion.get('platform_optimizations', []):
|
||||
st.write(f"- {platform}")
|
||||
st.write("**Content Variations**")
|
||||
for variation in suggestion.get('variations', []):
|
||||
st.write(f"- {variation}")
|
||||
st.write("**SEO Recommendations**")
|
||||
for seo in suggestion.get('seo_recommendations', []):
|
||||
st.write(f"- {seo}")
|
||||
if suggestion.get('media_suggestions'):
|
||||
st.write("**Media Suggestions**")
|
||||
for media in suggestion['media_suggestions']:
|
||||
st.write(f"- {media}")
|
||||
51
lib/content_calendar/ui/calendar_view.py
Normal file
51
lib/content_calendar/ui/calendar_view.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import streamlit as st
|
||||
from .components.content_card import render_content_card
|
||||
from .components.badge import render_badge
|
||||
|
||||
def render_calendar_view(calendar_data, icon_map, status_color, on_edit, on_delete, on_generate, get_item_key):
|
||||
if calendar_data is not None and not calendar_data.empty:
|
||||
st.markdown("### All Scheduled Content")
|
||||
calendar_data = calendar_data.sort_values(by="date")
|
||||
grouped = list(calendar_data.groupby(calendar_data['date'].dt.date))
|
||||
for i, (date, group) in enumerate(grouped):
|
||||
exp_open = (i == 0)
|
||||
with st.expander(f"{date.strftime('%B %d, %Y')}", expanded=exp_open):
|
||||
for idx, row in group.iterrows():
|
||||
item_key = get_item_key(row)
|
||||
is_editing = st.session_state.get("editing_item_key") == item_key
|
||||
platform = str(row['platform'])
|
||||
if hasattr(platform, 'value'):
|
||||
platform = platform.value
|
||||
platform_map = {
|
||||
'blog': 'Blog',
|
||||
'website': 'Blog',
|
||||
'instagram': 'Instagram',
|
||||
'twitter': 'Twitter',
|
||||
'linkedin': 'LinkedIn',
|
||||
'facebook': 'Facebook',
|
||||
}
|
||||
platform_disp = platform_map.get(platform.lower(), 'Blog')
|
||||
type_disp = str(row['type'])
|
||||
if hasattr(type_disp, 'value'):
|
||||
type_disp = type_disp.value
|
||||
type_disp = type_disp.replace('_', ' ').title()
|
||||
status_disp = row['status'].capitalize()
|
||||
platform_icon = icon_map.get(platform_disp, '🌐')
|
||||
type_icon = icon_map.get(type_disp, '📄')
|
||||
render_content_card(
|
||||
row=row,
|
||||
is_editing=is_editing,
|
||||
on_edit=lambda r=row: on_edit(r),
|
||||
on_delete=lambda r=row: on_delete(r),
|
||||
on_generate=lambda r=row: on_generate(r),
|
||||
icon_map=icon_map,
|
||||
status_color=status_color,
|
||||
platform_disp=platform_disp,
|
||||
type_disp=type_disp,
|
||||
status_disp=status_disp,
|
||||
platform_icon=platform_icon,
|
||||
type_icon=type_icon,
|
||||
item_key=item_key
|
||||
)
|
||||
else:
|
||||
st.info("No content scheduled yet. Add content to see it here.")
|
||||
294
lib/content_calendar/ui/components/ab_testing.py
Normal file
294
lib/content_calendar/ui/components/ab_testing.py
Normal file
@@ -0,0 +1,294 @@
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List
|
||||
from lib.database.models import ContentItem
|
||||
import logging
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
from lib.ai_seo_tools.content_calendar.core.calendar_manager import CalendarManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def render_ab_testing(content_generator: ContentGenerator, calendar_manager: CalendarManager):
|
||||
"""Render the A/B testing interface."""
|
||||
st.header("A/B Testing")
|
||||
|
||||
# Check if calendar manager is available
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.error("Calendar manager not initialized. Please refresh the page.")
|
||||
return
|
||||
|
||||
# Get available content
|
||||
try:
|
||||
available_content = calendar_manager.get_calendar().get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting content options: {str(e)}")
|
||||
st.error("Error loading content. Please try again.")
|
||||
return
|
||||
|
||||
if not content_options:
|
||||
st.info("""
|
||||
## Welcome to A/B Testing! 🧪
|
||||
|
||||
Test different versions of your content to find what works best. Here's what you can do:
|
||||
|
||||
### Features:
|
||||
- 🔄 **Variant Generation**: Create multiple versions of your content
|
||||
- 📊 **Performance Tracking**: Compare metrics across variants
|
||||
- 📈 **Statistical Analysis**: Get data-driven insights
|
||||
- 🎯 **Winner Selection**: Identify the best performing content
|
||||
|
||||
### Getting Started:
|
||||
1. First, add some content to your calendar
|
||||
2. Select the content you want to test
|
||||
3. Generate variants with different parameters
|
||||
4. Track performance and analyze results
|
||||
|
||||
Ready to get started? Add some content to your calendar first!
|
||||
""")
|
||||
return
|
||||
|
||||
# Content Selection
|
||||
selected_content = st.selectbox(
|
||||
"Select content to test",
|
||||
options=content_options,
|
||||
key="ab_test_content_select"
|
||||
)
|
||||
|
||||
if selected_content:
|
||||
try:
|
||||
content_item = next(
|
||||
item for item in available_content
|
||||
if item.title == selected_content
|
||||
)
|
||||
|
||||
# Show onboarding info if no test history
|
||||
if not st.session_state.get('ab_test_results', {}).get(content_item.title):
|
||||
st.info("""
|
||||
### A/B Testing Guide
|
||||
|
||||
Create and compare different versions of your content:
|
||||
|
||||
- **Headline Variations**: Test different titles and hooks
|
||||
- **Content Structure**: Try different content flows
|
||||
- **Call-to-Action**: Test various CTAs
|
||||
- **Visual Elements**: Compare different media placements
|
||||
|
||||
Click 'Generate Test Variants' to get started!
|
||||
""")
|
||||
|
||||
# Test Configuration
|
||||
st.markdown("### Create A/B Test")
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
test_content = st.selectbox(
|
||||
"Select content to A/B test",
|
||||
options=content_options,
|
||||
key="ab_test_content_select_unique"
|
||||
)
|
||||
|
||||
with col2:
|
||||
num_variants = st.slider(
|
||||
"Number of variants",
|
||||
min_value=2,
|
||||
max_value=5,
|
||||
value=2,
|
||||
help="Number of different versions to test"
|
||||
)
|
||||
|
||||
if test_content:
|
||||
content_item = next(
|
||||
item for item in calendar_manager.get_calendar().get_all_content()
|
||||
if item.title == test_content
|
||||
)
|
||||
|
||||
# Test Settings
|
||||
with st.expander("Test Settings"):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
test_duration = st.number_input(
|
||||
"Test Duration (days)",
|
||||
min_value=1,
|
||||
max_value=30,
|
||||
value=7
|
||||
)
|
||||
target_metric = st.selectbox(
|
||||
"Primary Metric",
|
||||
options=['Engagement', 'Conversion', 'Reach', 'Click-through'],
|
||||
index=0
|
||||
)
|
||||
with col2:
|
||||
audience_size = st.select_slider(
|
||||
"Audience Size",
|
||||
options=['Small', 'Medium', 'Large'],
|
||||
value='Medium'
|
||||
)
|
||||
confidence_level = st.slider(
|
||||
"Confidence Level",
|
||||
min_value=90,
|
||||
max_value=99,
|
||||
value=95,
|
||||
help="Statistical confidence level for test results"
|
||||
)
|
||||
|
||||
# Generate Variants
|
||||
if st.button("Generate Variants"):
|
||||
with st.spinner("Generating variants..."):
|
||||
variants = _generate_ab_test_variants(content_generator, content_item, num_variants)
|
||||
if variants:
|
||||
st.success(f"Generated {len(variants)} variants!")
|
||||
|
||||
# Display variants in tabs
|
||||
variant_tabs = st.tabs([f"Variant {i+1}" for i in range(len(variants))])
|
||||
for i, tab in enumerate(variant_tabs):
|
||||
with tab:
|
||||
st.markdown(f"### Variant {i+1}")
|
||||
st.json(variants[i]['content'])
|
||||
|
||||
# Variant metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric(
|
||||
"Engagement Score",
|
||||
f"{variants[i]['metrics']['engagement_score']:.1f}%"
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Conversion Rate",
|
||||
f"{variants[i]['metrics']['conversion_rate']:.1f}%"
|
||||
)
|
||||
with col3:
|
||||
st.metric(
|
||||
"Reach",
|
||||
f"{variants[i]['metrics']['reach']:,}"
|
||||
)
|
||||
|
||||
# Results Analysis
|
||||
st.markdown("### Analyze Results")
|
||||
if test_content in st.session_state.ab_test_results:
|
||||
test_data = st.session_state.ab_test_results[test_content]
|
||||
|
||||
# Test Status
|
||||
st.info(f"Test Status: {test_data['status']}")
|
||||
st.write(f"Started: {test_data['start_time']}")
|
||||
|
||||
if test_data['status'] == 'running':
|
||||
if st.button("End Test and Analyze"):
|
||||
with st.spinner("Analyzing results..."):
|
||||
results = _analyze_ab_test_results(content_item)
|
||||
if results:
|
||||
st.success("Analysis complete!")
|
||||
_display_test_results(results)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in A/B testing interface: {str(e)}", exc_info=True)
|
||||
st.error(f"Error in A/B testing: {str(e)}")
|
||||
|
||||
def _generate_ab_test_variants(
|
||||
content_generator,
|
||||
content: ContentItem,
|
||||
num_variants: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate A/B test variants for content."""
|
||||
try:
|
||||
logger.info(f"Generating {num_variants} variants for content: {content.title}")
|
||||
|
||||
# Convert content to dictionary format
|
||||
content_dict = {
|
||||
'title': content.title,
|
||||
'content': content.description,
|
||||
'metadata': {
|
||||
'platform': content.platforms[0].name if content.platforms else 'Unknown',
|
||||
'content_type': content.content_type.name
|
||||
}
|
||||
}
|
||||
|
||||
variants = []
|
||||
for i in range(num_variants):
|
||||
# Generate different variations
|
||||
variant = content_generator.generate_variation(
|
||||
content=content_dict,
|
||||
variation_type=f"variant_{i+1}"
|
||||
)
|
||||
if variant:
|
||||
variants.append(variant)
|
||||
|
||||
return variants
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating variants: {str(e)}")
|
||||
return []
|
||||
|
||||
def _analyze_ab_test_results(content_item: ContentItem) -> Dict[str, Any]:
|
||||
"""Analyze results of A/B testing for content optimization."""
|
||||
try:
|
||||
logger.info(f"Analyzing A/B test results for: {content_item.title}")
|
||||
|
||||
if content_item.title not in st.session_state.ab_test_results:
|
||||
raise ValueError("No A/B test results found for this content")
|
||||
|
||||
test_data = st.session_state.ab_test_results[content_item.title]
|
||||
variants = test_data['variants']
|
||||
|
||||
# Calculate performance metrics
|
||||
results = {
|
||||
'total_engagement': sum(v['metrics']['engagement_score'] for v in variants),
|
||||
'total_conversions': sum(v['metrics']['conversion_rate'] for v in variants),
|
||||
'total_reach': sum(v['metrics']['reach'] for v in variants),
|
||||
'best_performing_variant': max(variants, key=lambda x: x['metrics']['engagement_score']),
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
# Generate recommendations
|
||||
for variant in variants:
|
||||
if variant['metrics']['engagement_score'] > 0.7: # High engagement threshold
|
||||
results['recommendations'].append({
|
||||
'variant_id': variant['variant_id'],
|
||||
'reason': 'High engagement score',
|
||||
'suggested_actions': ['Scale this variant', 'Apply learnings to other content']
|
||||
})
|
||||
|
||||
# Update test status
|
||||
test_data['status'] = 'completed'
|
||||
test_data['results'] = results
|
||||
|
||||
logger.info("A/B test results analyzed successfully")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing A/B test results: {str(e)}", exc_info=True)
|
||||
st.error(f"Error analyzing A/B test results: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _display_test_results(results: Dict[str, Any]) -> None:
|
||||
"""Display A/B test results in the UI."""
|
||||
with st.expander("Overall Performance", expanded=True):
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric(
|
||||
"Total Engagement",
|
||||
f"{results['total_engagement']:.1f}%"
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Total Conversions",
|
||||
f"{results['total_conversions']:.1f}%"
|
||||
)
|
||||
with col3:
|
||||
st.metric(
|
||||
"Total Reach",
|
||||
f"{results['total_reach']:,}"
|
||||
)
|
||||
|
||||
with st.expander("Best Performing Variant", expanded=True):
|
||||
best_variant = results['best_performing_variant']
|
||||
st.markdown(f"### {best_variant['variant_id']}")
|
||||
st.json(best_variant['content'])
|
||||
|
||||
with st.expander("Recommendations", expanded=True):
|
||||
for rec in results['recommendations']:
|
||||
st.markdown(f"#### {rec['variant_id']}")
|
||||
st.write(f"Reason: {rec['reason']}")
|
||||
st.write("Suggested Actions:")
|
||||
for action in rec['suggested_actions']:
|
||||
st.write(f"- {action}")
|
||||
2
lib/content_calendar/ui/components/badge.py
Normal file
2
lib/content_calendar/ui/components/badge.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def render_badge(platform_disp, platform_icon, type_disp, status_disp):
|
||||
return f"<span class='badge-content-calendar badge-platform-{platform_disp.lower()}'>{platform_icon} {platform_disp} | {type_disp} | <span class='chip-status chip-status-{status_disp.lower()}'>{status_disp}</span></span>"
|
||||
22
lib/content_calendar/ui/components/content_card.py
Normal file
22
lib/content_calendar/ui/components/content_card.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import streamlit as st
|
||||
|
||||
def render_content_card(row, is_editing, on_edit, on_delete, on_generate, icon_map, status_color, platform_disp, type_disp, status_disp, platform_icon, type_icon, item_key):
|
||||
st.markdown(f"<div class='card-content-calendar'>", unsafe_allow_html=True)
|
||||
st.markdown(f"<div style='display:flex;align-items:center;justify-content:space-between;gap:8px;'>", unsafe_allow_html=True)
|
||||
st.markdown(f"<div style='display:flex;align-items:center;gap:8px;min-width:0;flex:1;'>"
|
||||
f"{type_icon}<span class='content-title'>{row['title']}</span></div>", unsafe_allow_html=True)
|
||||
st.markdown("<div style='display:flex;align-items:center;gap:4px;'>", unsafe_allow_html=True)
|
||||
col1, col2, col3 = st.columns([1, 1, 1])
|
||||
with col1:
|
||||
if st.button("⚡", key=f"generate_{item_key}", help="Generate with AI Blog Writer", use_container_width=True):
|
||||
on_generate()
|
||||
with col2:
|
||||
if st.button("✏️", key=f"edit_{item_key}", help="Edit Content", use_container_width=True):
|
||||
on_edit()
|
||||
with col3:
|
||||
if st.button("🗑️", key=f"delete_{item_key}", help="Delete Content", use_container_width=True):
|
||||
on_delete()
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
st.markdown(f"<div class='content-meta'><span class='badge-content-calendar badge-platform-{platform_disp.lower()}'>{platform_icon} {platform_disp} | {type_disp} | <span class='chip-status chip-status-{status_disp.lower()}'>{status_disp}</span></span></div>", unsafe_allow_html=True)
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
498
lib/content_calendar/ui/components/content_optimization.py
Normal file
498
lib/content_calendar/ui/components/content_optimization.py
Normal file
@@ -0,0 +1,498 @@
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
from lib.ai_seo_tools.content_calendar.core.ai_generator import AIGenerator
|
||||
from lib.ai_seo_tools.content_calendar.integrations.seo_optimizer import SEOOptimizer
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
import logging
|
||||
from lib.database.models import get_engine, get_session, init_db
|
||||
|
||||
logger = logging.getLogger('content_calendar.optimization')
|
||||
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
class OptimizationManager:
|
||||
def __init__(self):
|
||||
if 'optimization_history' not in st.session_state:
|
||||
st.session_state.optimization_history = {}
|
||||
if 'optimization_previews' not in st.session_state:
|
||||
st.session_state.optimization_previews = {}
|
||||
if 'optimization_metrics' not in st.session_state:
|
||||
st.session_state.optimization_metrics = {}
|
||||
|
||||
def track_optimization(self, content_id: str, optimization_data: Dict[str, Any]) -> bool:
|
||||
"""Track optimization changes for content with detailed metrics."""
|
||||
try:
|
||||
if content_id not in st.session_state.optimization_history:
|
||||
st.session_state.optimization_history[content_id] = []
|
||||
|
||||
optimization_data['timestamp'] = datetime.now()
|
||||
optimization_data['metrics'] = self._calculate_optimization_metrics(optimization_data)
|
||||
st.session_state.optimization_history[content_id].append(optimization_data)
|
||||
|
||||
# Update metrics
|
||||
if content_id not in st.session_state.optimization_metrics:
|
||||
st.session_state.optimization_metrics[content_id] = []
|
||||
st.session_state.optimization_metrics[content_id].append(optimization_data['metrics'])
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error tracking optimization: {str(e)}")
|
||||
return False
|
||||
|
||||
def _calculate_optimization_metrics(self, optimization_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Calculate detailed optimization metrics."""
|
||||
try:
|
||||
metrics = {
|
||||
'readability_score': 0,
|
||||
'seo_score': 0,
|
||||
'engagement_potential': 0,
|
||||
'keyword_density': 0,
|
||||
'content_quality': 0
|
||||
}
|
||||
|
||||
# Calculate readability score
|
||||
if 'content' in optimization_data:
|
||||
content = optimization_data['content']
|
||||
metrics['readability_score'] = self._calculate_readability(content)
|
||||
|
||||
# Calculate SEO score
|
||||
if 'seo_data' in optimization_data:
|
||||
seo_data = optimization_data['seo_data']
|
||||
metrics['seo_score'] = self._calculate_seo_score(seo_data)
|
||||
metrics['keyword_density'] = self._calculate_keyword_density(seo_data)
|
||||
|
||||
# Calculate engagement potential
|
||||
if 'engagement_metrics' in optimization_data:
|
||||
engagement = optimization_data['engagement_metrics']
|
||||
metrics['engagement_potential'] = self._calculate_engagement_potential(engagement)
|
||||
|
||||
# Calculate overall content quality
|
||||
metrics['content_quality'] = (
|
||||
metrics['readability_score'] * 0.3 +
|
||||
metrics['seo_score'] * 0.3 +
|
||||
metrics['engagement_potential'] * 0.4
|
||||
)
|
||||
|
||||
return metrics
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating optimization metrics: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _calculate_readability(self, content: str) -> float:
|
||||
"""Calculate content readability score."""
|
||||
try:
|
||||
# Implement readability calculation logic
|
||||
# This is a placeholder implementation
|
||||
return 0.8
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating readability: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
def _calculate_seo_score(self, seo_data: SEOData) -> float:
|
||||
"""Calculate SEO optimization score."""
|
||||
try:
|
||||
# Implement SEO score calculation logic
|
||||
# This is a placeholder implementation
|
||||
return 0.85
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating SEO score: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
def _calculate_keyword_density(self, seo_data: SEOData) -> float:
|
||||
"""Calculate keyword density."""
|
||||
try:
|
||||
# Implement keyword density calculation logic
|
||||
# This is a placeholder implementation
|
||||
return 2.5
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating keyword density: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
def _calculate_engagement_potential(self, engagement: Dict[str, Any]) -> float:
|
||||
"""Calculate content engagement potential."""
|
||||
try:
|
||||
# Implement engagement potential calculation logic
|
||||
# This is a placeholder implementation
|
||||
return 0.75
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating engagement potential: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
def get_optimization_history(self, content_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get detailed optimization history for content."""
|
||||
return st.session_state.optimization_history.get(content_id, [])
|
||||
|
||||
def get_optimization_metrics(self, content_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get optimization metrics history."""
|
||||
return st.session_state.optimization_metrics.get(content_id, [])
|
||||
|
||||
def save_preview(self, content_id: str, preview_data: Dict[str, Any]) -> bool:
|
||||
"""Save optimization preview with versioning."""
|
||||
try:
|
||||
if content_id not in st.session_state.optimization_previews:
|
||||
st.session_state.optimization_previews[content_id] = []
|
||||
|
||||
preview_data['version'] = len(st.session_state.optimization_previews[content_id]) + 1
|
||||
preview_data['timestamp'] = datetime.now()
|
||||
st.session_state.optimization_previews[content_id].append(preview_data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving preview: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_preview(self, content_id: str, version: int = None) -> Dict[str, Any]:
|
||||
"""Get optimization preview with optional versioning."""
|
||||
try:
|
||||
previews = st.session_state.optimization_previews.get(content_id, [])
|
||||
if not previews:
|
||||
return {}
|
||||
|
||||
if version is None:
|
||||
return previews[-1]
|
||||
|
||||
for preview in previews:
|
||||
if preview['version'] == version:
|
||||
return preview
|
||||
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting preview: {str(e)}")
|
||||
return {}
|
||||
|
||||
def render_content_optimization(
|
||||
content_generator: ContentGenerator,
|
||||
ai_generator: AIGenerator,
|
||||
seo_optimizer: SEOOptimizer
|
||||
):
|
||||
"""Render the content optimization interface with advanced features."""
|
||||
st.title("Content Calendar")
|
||||
|
||||
# Initialize optimization manager
|
||||
optimization_manager = OptimizationManager()
|
||||
|
||||
# Check if calendar manager is available
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.error("Calendar manager not initialized. Please refresh the page.")
|
||||
return
|
||||
|
||||
# Create main tabs
|
||||
main_tabs = st.tabs(["Content Planning", "Content Optimization"])
|
||||
|
||||
with main_tabs[0]:
|
||||
# Create two columns for the layout
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
st.header("Quick Calendar Generation")
|
||||
st.markdown("""
|
||||
Generate a content calendar in three simple steps:
|
||||
1. Enter your keywords
|
||||
2. Select target platforms
|
||||
3. Choose time period
|
||||
""")
|
||||
|
||||
# Step 1: Keywords Input
|
||||
st.subheader("Step 1: Enter Keywords")
|
||||
keywords = st.text_area(
|
||||
"Enter keywords or topics (one per line)",
|
||||
help="Enter the main topics or keywords you want to create content about"
|
||||
)
|
||||
|
||||
# Step 2: Platform Selection
|
||||
st.subheader("Step 2: Select Target Platforms")
|
||||
platform_categories = {
|
||||
"Website": ["WEBSITE"],
|
||||
"Social Media": ["INSTAGRAM", "FACEBOOK", "TWITTER", "LINKEDIN"],
|
||||
"Video": ["YOUTUBE"],
|
||||
"Newsletter": ["NEWSLETTER"]
|
||||
}
|
||||
|
||||
selected_platforms = []
|
||||
for category, platforms in platform_categories.items():
|
||||
st.markdown(f"**{category}**")
|
||||
for platform in platforms:
|
||||
if st.checkbox(platform.replace("_", " ").title(), key=f"platform_{platform}"):
|
||||
selected_platforms.append(platform)
|
||||
|
||||
# Step 3: Time Period
|
||||
st.subheader("Step 3: Choose Time Period")
|
||||
time_period = st.selectbox(
|
||||
"Select time period",
|
||||
["1 Week", "2 Weeks", "1 Month", "3 Months", "6 Months"],
|
||||
help="Choose how far ahead you want to plan your content"
|
||||
)
|
||||
|
||||
# Generate Calendar Button
|
||||
if st.button("Generate with AI", type="primary"):
|
||||
if not keywords or not selected_platforms:
|
||||
st.error("Please enter keywords and select at least one platform.")
|
||||
else:
|
||||
with st.spinner("Generating content calendar..."):
|
||||
try:
|
||||
# Generate content ideas based on keywords
|
||||
content_ideas = []
|
||||
for keyword in keywords.split('\n'):
|
||||
if keyword.strip():
|
||||
# Generate content ideas for each platform
|
||||
for platform in selected_platforms:
|
||||
try:
|
||||
# Create a content item for the AI generator
|
||||
content_item = ContentItem(
|
||||
title=keyword.strip(),
|
||||
description=f"Content about {keyword.strip()}",
|
||||
content_type=ContentType.BLOG_POST if platform == "WEBSITE" else ContentType.SOCIAL_MEDIA,
|
||||
platforms=[Platform[platform]],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=keyword.strip(),
|
||||
meta_description=f"Content about {keyword.strip()}",
|
||||
keywords=[keyword.strip()],
|
||||
structured_data={}
|
||||
)
|
||||
)
|
||||
|
||||
# Generate content using AI generator
|
||||
content_idea = ai_generator.enhance_content(
|
||||
content=content_item,
|
||||
enhancement_type='content_generation',
|
||||
target_audience={
|
||||
'content_settings': {
|
||||
'tone': 'professional',
|
||||
'length': 'medium',
|
||||
'engagement_goal': 'awareness',
|
||||
'creativity_level': 5
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if content_idea:
|
||||
content_ideas.append({
|
||||
'title': content_idea.get('title', keyword.strip()),
|
||||
'introduction': content_idea.get('content', f"Content about {keyword.strip()}"),
|
||||
'platform': platform,
|
||||
'meta_description': content_idea.get('meta_description', ''),
|
||||
'keywords': [keyword.strip()]
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content for {keyword} on {platform}: {str(e)}")
|
||||
continue
|
||||
|
||||
if content_ideas:
|
||||
# Create calendar entries
|
||||
calendar = st.session_state.calendar_manager.get_calendar()
|
||||
for idea in content_ideas:
|
||||
try:
|
||||
# Create content item
|
||||
content_item = ContentItem(
|
||||
title=idea['title'],
|
||||
description=idea['introduction'],
|
||||
content_type=ContentType.BLOG_POST if idea['platform'] == "WEBSITE" else ContentType.SOCIAL_MEDIA,
|
||||
platforms=[Platform[idea['platform']]],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=idea['title'],
|
||||
meta_description=idea.get('meta_description', ''),
|
||||
keywords=idea.get('keywords', []),
|
||||
structured_data={}
|
||||
)
|
||||
)
|
||||
calendar.add_content(content_item)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding content to calendar: {str(e)}")
|
||||
continue
|
||||
|
||||
st.success("Content calendar generated successfully!")
|
||||
st.rerun() # Refresh to show new content
|
||||
else:
|
||||
st.error("Failed to generate any content ideas. Please try different keywords or settings.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content calendar: {str(e)}")
|
||||
st.error("An error occurred while generating the content calendar. Please try again.")
|
||||
|
||||
with col2:
|
||||
st.header("Scheduled Content")
|
||||
# Get all content from calendar
|
||||
calendar = st.session_state.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
st.info("No content scheduled yet. Generate content using the form on the left.")
|
||||
else:
|
||||
# Group content by platform
|
||||
platform_content = {}
|
||||
for item in calendar.get_all_content():
|
||||
platform = item.platforms[0].name if item.platforms else "Unknown"
|
||||
if platform not in platform_content:
|
||||
platform_content[platform] = []
|
||||
platform_content[platform].append(item)
|
||||
|
||||
# Create tabs for each platform
|
||||
platform_tabs = st.tabs(list(platform_content.keys()))
|
||||
|
||||
for i, (platform, content) in enumerate(platform_content.items()):
|
||||
with platform_tabs[i]:
|
||||
st.write(f"### {platform} Content")
|
||||
|
||||
# Convert content to DataFrame for better display
|
||||
content_data = []
|
||||
for item in content:
|
||||
content_data.append({
|
||||
'Date': item.publish_date.strftime('%Y-%m-%d'),
|
||||
'Title': item.title,
|
||||
'Type': item.content_type.name,
|
||||
'Status': item.status
|
||||
})
|
||||
|
||||
if content_data:
|
||||
df = pd.DataFrame(content_data)
|
||||
st.dataframe(df, use_container_width=True)
|
||||
|
||||
# Add action buttons for each content item
|
||||
for item in content:
|
||||
with st.expander(f"Actions for: {item.title}"):
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
if st.button("Edit", key=f"edit_{item.title}"):
|
||||
st.session_state.selected_content = item.title
|
||||
with col2:
|
||||
if st.button("Optimize", key=f"optimize_{item.title}"):
|
||||
st.session_state.selected_content = item.title
|
||||
st.session_state.active_tab = "Content Optimization"
|
||||
with col3:
|
||||
if st.button("Delete", key=f"delete_{item.title}"):
|
||||
calendar.remove_content(item)
|
||||
st.success(f"Removed {item.title}")
|
||||
st.rerun()
|
||||
|
||||
with main_tabs[1]:
|
||||
st.header("Content Optimization")
|
||||
# Get available content
|
||||
calendar = st.session_state.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
st.info("No content available for optimization. Use the Content Planning tab to generate content.")
|
||||
return
|
||||
|
||||
available_content = calendar.get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
|
||||
# Content selection
|
||||
selected_content = st.selectbox(
|
||||
"Select content to optimize",
|
||||
options=content_options,
|
||||
key="optimize_content_select"
|
||||
)
|
||||
|
||||
if selected_content:
|
||||
try:
|
||||
content_item = next(
|
||||
item for item in available_content
|
||||
if item.title == selected_content
|
||||
)
|
||||
|
||||
# Create tabs for different optimization aspects
|
||||
opt_tabs = st.tabs(["Content Optimization", "SEO Optimization", "Preview", "History", "Analytics"])
|
||||
|
||||
with opt_tabs[0]:
|
||||
st.subheader("Content Optimization")
|
||||
|
||||
# Show onboarding info if no optimization history
|
||||
if not optimization_manager.get_optimization_history(content_item.title):
|
||||
st.info("""
|
||||
### Content Optimization Guide
|
||||
|
||||
Use these tools to enhance your content:
|
||||
|
||||
- **Content Tone**: Adjust the writing style to match your brand voice
|
||||
- **Content Length**: Optimize for your target platform's requirements
|
||||
- **Engagement Goal**: Focus on specific audience actions
|
||||
- **Creativity Level**: Balance between creative and professional content
|
||||
|
||||
Click 'Generate Optimization' to get started!
|
||||
""")
|
||||
|
||||
# Advanced Optimization Settings
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
tone = st.select_slider(
|
||||
"Content Tone",
|
||||
options=["Professional", "Casual", "Educational", "Entertaining", "Persuasive"],
|
||||
value="Professional"
|
||||
)
|
||||
length = st.radio(
|
||||
"Content Length",
|
||||
["Short", "Medium", "Long"],
|
||||
horizontal=True
|
||||
)
|
||||
with col2:
|
||||
engagement_goal = st.selectbox(
|
||||
"Engagement Goal",
|
||||
["Awareness", "Consideration", "Conversion", "Retention"]
|
||||
)
|
||||
creativity_level = st.slider(
|
||||
"Creativity Level",
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
value=5
|
||||
)
|
||||
|
||||
if st.button("Generate Optimization", type="primary"):
|
||||
with st.spinner("Optimizing content..."):
|
||||
try:
|
||||
# Generate optimization
|
||||
optimization = content_generator.optimize_content(
|
||||
content=content_item,
|
||||
tone=tone,
|
||||
length=length,
|
||||
engagement_goal=engagement_goal,
|
||||
creativity_level=creativity_level
|
||||
)
|
||||
|
||||
if optimization:
|
||||
st.success("Content optimized successfully!")
|
||||
|
||||
# Show optimization results
|
||||
st.subheader("Optimization Results")
|
||||
st.write(optimization.get('content', ''))
|
||||
|
||||
# Save optimization history
|
||||
optimization_manager.track_optimization(
|
||||
content_item.title,
|
||||
{
|
||||
'tone': tone,
|
||||
'length': length,
|
||||
'engagement_goal': engagement_goal,
|
||||
'creativity_level': creativity_level,
|
||||
'content': optimization.get('content', ''),
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
)
|
||||
else:
|
||||
st.error("Failed to optimize content. Please try again.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing content: {str(e)}")
|
||||
st.error("An error occurred while optimizing content. Please try again.")
|
||||
|
||||
with opt_tabs[1]:
|
||||
st.subheader("SEO Optimization")
|
||||
# SEO optimization content here
|
||||
|
||||
with opt_tabs[2]:
|
||||
st.subheader("Content Preview")
|
||||
# Content preview here
|
||||
|
||||
with opt_tabs[3]:
|
||||
st.subheader("Optimization History")
|
||||
# Optimization history here
|
||||
|
||||
with opt_tabs[4]:
|
||||
st.subheader("Performance Analytics")
|
||||
# Analytics content here
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing selected content: {str(e)}")
|
||||
st.error("Error processing selected content. Please try again.")
|
||||
|
||||
# Remove everything after this point
|
||||
517
lib/content_calendar/ui/components/content_repurposing_ui.py
Normal file
517
lib/content_calendar/ui/components/content_repurposing_ui.py
Normal file
@@ -0,0 +1,517 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Add parent directory to path to import existing tools
|
||||
parent_dir = str(Path(__file__).parent.parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
from lib.ai_seo_tools.content_calendar.core.content_repurposer import SmartContentRepurposingEngine
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentRepurposingUI:
|
||||
"""
|
||||
Streamlit UI component for the Smart Content Repurposing Engine.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.repurposing_engine = SmartContentRepurposingEngine()
|
||||
self.content_generator = ContentGenerator()
|
||||
self.logger = logging.getLogger('content_calendar.repurposing_ui')
|
||||
|
||||
def render_repurposing_interface(self):
|
||||
"""Render the main repurposing interface."""
|
||||
st.header("🔄 Smart Content Repurposing Engine")
|
||||
st.markdown("Transform your content into multiple platform-optimized pieces with AI-powered repurposing.")
|
||||
|
||||
# Create tabs for different repurposing functions
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"📝 Single Content Repurposing",
|
||||
"📚 Content Series Creation",
|
||||
"🔍 Content Analysis",
|
||||
"📊 Repurposing Dashboard"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
self._render_single_content_repurposing()
|
||||
|
||||
with tab2:
|
||||
self._render_content_series_creation()
|
||||
|
||||
with tab3:
|
||||
self._render_content_analysis()
|
||||
|
||||
with tab4:
|
||||
self._render_repurposing_dashboard()
|
||||
|
||||
def _render_single_content_repurposing(self):
|
||||
"""Render the single content repurposing interface."""
|
||||
st.subheader("Repurpose Single Content")
|
||||
st.markdown("Transform one piece of content into multiple platform-optimized variations.")
|
||||
|
||||
# Content input section
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown("### 📄 Source Content")
|
||||
|
||||
# Content input options
|
||||
input_method = st.radio(
|
||||
"How would you like to provide content?",
|
||||
["Manual Input", "Upload File", "Select from Calendar"],
|
||||
horizontal=True
|
||||
)
|
||||
|
||||
source_content = None
|
||||
|
||||
if input_method == "Manual Input":
|
||||
source_content = self._render_manual_content_input()
|
||||
elif input_method == "Upload File":
|
||||
source_content = self._render_file_upload_input()
|
||||
else: # Select from Calendar
|
||||
source_content = self._render_calendar_selection()
|
||||
|
||||
with col2:
|
||||
st.markdown("### 🎯 Target Platforms")
|
||||
|
||||
# Platform selection
|
||||
available_platforms = [
|
||||
Platform.TWITTER,
|
||||
Platform.LINKEDIN,
|
||||
Platform.INSTAGRAM,
|
||||
Platform.FACEBOOK,
|
||||
Platform.WEBSITE
|
||||
]
|
||||
|
||||
selected_platforms = st.multiselect(
|
||||
"Select target platforms:",
|
||||
options=available_platforms,
|
||||
default=[Platform.TWITTER, Platform.LINKEDIN],
|
||||
format_func=lambda x: x.name.title()
|
||||
)
|
||||
|
||||
# Repurposing strategy
|
||||
strategy = st.selectbox(
|
||||
"Repurposing Strategy:",
|
||||
["adaptive", "atomic", "series"],
|
||||
help="Adaptive: AI chooses best approach, Atomic: Break into small pieces, Series: Create connected content"
|
||||
)
|
||||
|
||||
# Generate repurposed content
|
||||
if st.button("🚀 Generate Repurposed Content", type="primary"):
|
||||
if source_content and selected_platforms:
|
||||
with st.spinner("Repurposing content..."):
|
||||
try:
|
||||
repurposed_content = self.content_generator.repurpose_content_for_platforms(
|
||||
content_item=source_content,
|
||||
target_platforms=selected_platforms,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
if repurposed_content:
|
||||
self._display_repurposed_content(repurposed_content)
|
||||
else:
|
||||
st.error("Failed to generate repurposed content. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error during repurposing: {str(e)}")
|
||||
else:
|
||||
st.warning("Please provide source content and select at least one target platform.")
|
||||
|
||||
def _render_content_series_creation(self):
|
||||
"""Render the content series creation interface."""
|
||||
st.subheader("Create Cross-Platform Content Series")
|
||||
st.markdown("Generate a strategic content series that progressively reveals information across platforms.")
|
||||
|
||||
# Source content input
|
||||
source_content = self._render_manual_content_input(key_suffix="_series")
|
||||
|
||||
if source_content:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("### 🌐 Platform Strategy")
|
||||
|
||||
# Platform selection with strategy
|
||||
platforms = st.multiselect(
|
||||
"Select platforms for series:",
|
||||
options=[Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM, Platform.FACEBOOK, Platform.WEBSITE],
|
||||
default=[Platform.TWITTER, Platform.LINKEDIN, Platform.WEBSITE],
|
||||
format_func=lambda x: x.name.title(),
|
||||
key="series_platforms"
|
||||
)
|
||||
|
||||
series_type = st.selectbox(
|
||||
"Series Strategy:",
|
||||
["progressive_disclosure", "platform_native"],
|
||||
help="Progressive: Gradually reveal info across platforms, Native: Optimize for each platform's strengths"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("### 📅 Timeline Preview")
|
||||
|
||||
if platforms:
|
||||
# Show timeline preview
|
||||
timeline_df = self._create_series_timeline_preview(source_content, platforms)
|
||||
st.dataframe(timeline_df, use_container_width=True)
|
||||
|
||||
# Generate series
|
||||
if st.button("📚 Create Content Series", type="primary", key="create_series"):
|
||||
if platforms:
|
||||
with st.spinner("Creating content series..."):
|
||||
try:
|
||||
series_content = self.content_generator.create_content_series_across_platforms(
|
||||
source_content=source_content,
|
||||
platforms=platforms,
|
||||
series_type=series_type
|
||||
)
|
||||
|
||||
if series_content:
|
||||
self._display_content_series(series_content)
|
||||
else:
|
||||
st.error("Failed to create content series. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error creating series: {str(e)}")
|
||||
else:
|
||||
st.warning("Please select at least one platform for the series.")
|
||||
|
||||
def _render_content_analysis(self):
|
||||
"""Render the content analysis interface."""
|
||||
st.subheader("Content Repurposing Analysis")
|
||||
st.markdown("Analyze your content's repurposing potential and get AI-powered recommendations.")
|
||||
|
||||
# Content input
|
||||
content_to_analyze = self._render_manual_content_input(key_suffix="_analysis")
|
||||
|
||||
if content_to_analyze:
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
available_platforms = st.multiselect(
|
||||
"Available platforms for analysis:",
|
||||
options=[Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM, Platform.FACEBOOK, Platform.WEBSITE],
|
||||
default=[Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM, Platform.FACEBOOK, Platform.WEBSITE],
|
||||
format_func=lambda x: x.name.title(),
|
||||
key="analysis_platforms"
|
||||
)
|
||||
|
||||
with col2:
|
||||
if st.button("🔍 Analyze Content", type="primary"):
|
||||
if available_platforms:
|
||||
with st.spinner("Analyzing content..."):
|
||||
try:
|
||||
analysis = self.content_generator.analyze_content_for_repurposing(
|
||||
content_item=content_to_analyze,
|
||||
available_platforms=available_platforms
|
||||
)
|
||||
|
||||
if analysis:
|
||||
self._display_content_analysis(analysis)
|
||||
else:
|
||||
st.error("Failed to analyze content. Please try again.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error during analysis: {str(e)}")
|
||||
else:
|
||||
st.warning("Please select at least one platform for analysis.")
|
||||
|
||||
def _render_repurposing_dashboard(self):
|
||||
"""Render the repurposing dashboard with metrics and insights."""
|
||||
st.subheader("Repurposing Dashboard")
|
||||
st.markdown("Track your content repurposing performance and insights.")
|
||||
|
||||
# Mock data for demonstration
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Content Pieces Created", "156", "+23")
|
||||
|
||||
with col2:
|
||||
st.metric("Time Saved", "312 hours", "+45 hours")
|
||||
|
||||
with col3:
|
||||
st.metric("Platform Coverage", "85%", "+12%")
|
||||
|
||||
with col4:
|
||||
st.metric("Engagement Boost", "34%", "+8%")
|
||||
|
||||
# Recent repurposing activity
|
||||
st.markdown("### 📈 Recent Repurposing Activity")
|
||||
|
||||
# Mock data for recent activity
|
||||
recent_activity = pd.DataFrame({
|
||||
'Date': ['2024-01-15', '2024-01-14', '2024-01-13', '2024-01-12'],
|
||||
'Source Content': ['AI Writing Tips', 'SEO Best Practices', 'Content Strategy Guide', 'Social Media Trends'],
|
||||
'Platforms': ['Twitter, LinkedIn', 'LinkedIn, Instagram', 'All Platforms', 'Twitter, Facebook'],
|
||||
'Pieces Created': [3, 2, 5, 2],
|
||||
'Status': ['Published', 'Scheduled', 'Draft', 'Published']
|
||||
})
|
||||
|
||||
st.dataframe(recent_activity, use_container_width=True)
|
||||
|
||||
# Performance insights
|
||||
st.markdown("### 💡 Performance Insights")
|
||||
|
||||
insights_col1, insights_col2 = st.columns(2)
|
||||
|
||||
with insights_col1:
|
||||
st.info("🎯 **Best Performing Platform**: LinkedIn posts show 45% higher engagement when repurposed from blog content.")
|
||||
|
||||
with insights_col2:
|
||||
st.success("📊 **Optimization Tip**: Twitter threads perform 60% better when created from long-form content with statistics.")
|
||||
|
||||
def _render_manual_content_input(self, key_suffix: str = "") -> Optional[ContentItem]:
|
||||
"""Render manual content input form."""
|
||||
with st.form(f"content_input_form{key_suffix}"):
|
||||
title = st.text_input("Content Title:", key=f"title{key_suffix}")
|
||||
content_type = st.selectbox(
|
||||
"Content Type:",
|
||||
options=[ContentType.BLOG_POST, ContentType.SOCIAL_MEDIA, ContentType.VIDEO, ContentType.NEWSLETTER],
|
||||
format_func=lambda x: x.name.replace('_', ' ').title(),
|
||||
key=f"content_type{key_suffix}"
|
||||
)
|
||||
|
||||
description = st.text_area(
|
||||
"Content Description/Body:",
|
||||
height=200,
|
||||
help="Paste your content here. This will be analyzed and repurposed.",
|
||||
key=f"description{key_suffix}"
|
||||
)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
author = st.text_input("Author:", value="Content Creator", key=f"author{key_suffix}")
|
||||
with col2:
|
||||
tags = st.text_input("Tags (comma-separated):", key=f"tags{key_suffix}")
|
||||
|
||||
submitted = st.form_submit_button("📝 Use This Content")
|
||||
|
||||
if submitted and title and description:
|
||||
# Create ContentItem
|
||||
content_item = ContentItem(
|
||||
title=title,
|
||||
description=description,
|
||||
content_type=content_type,
|
||||
platforms=[],
|
||||
publish_date=datetime.now(),
|
||||
status="draft",
|
||||
author=author,
|
||||
tags=tags.split(',') if tags else [],
|
||||
notes="",
|
||||
seo_data=SEOData(title=title, meta_description="", keywords=[], structured_data={})
|
||||
)
|
||||
return content_item
|
||||
|
||||
return None
|
||||
|
||||
def _render_file_upload_input(self) -> Optional[ContentItem]:
|
||||
"""Render file upload input."""
|
||||
uploaded_file = st.file_uploader(
|
||||
"Upload content file:",
|
||||
type=['txt', 'md', 'docx'],
|
||||
help="Upload a text file, markdown file, or Word document"
|
||||
)
|
||||
|
||||
if uploaded_file:
|
||||
try:
|
||||
# Read file content
|
||||
if uploaded_file.type == "text/plain":
|
||||
content = str(uploaded_file.read(), "utf-8")
|
||||
else:
|
||||
content = str(uploaded_file.read(), "utf-8") # Simplified for demo
|
||||
|
||||
# Extract title from filename
|
||||
title = uploaded_file.name.split('.')[0].replace('_', ' ').title()
|
||||
|
||||
# Create ContentItem
|
||||
content_item = ContentItem(
|
||||
title=title,
|
||||
description=content,
|
||||
content_type=ContentType.BLOG_POST,
|
||||
platforms=[],
|
||||
publish_date=datetime.now(),
|
||||
status="draft",
|
||||
author="Uploaded Content",
|
||||
tags=[],
|
||||
notes=f"Uploaded from file: {uploaded_file.name}",
|
||||
seo_data=SEOData(title=title, meta_description="", keywords=[], structured_data={})
|
||||
)
|
||||
|
||||
st.success(f"✅ File uploaded: {uploaded_file.name}")
|
||||
return content_item
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error reading file: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
def _render_calendar_selection(self) -> Optional[ContentItem]:
|
||||
"""Render calendar content selection."""
|
||||
st.info("📅 Calendar integration coming soon! For now, please use manual input or file upload.")
|
||||
return None
|
||||
|
||||
def _display_repurposed_content(self, repurposed_content: List[ContentItem]):
|
||||
"""Display the repurposed content results."""
|
||||
st.success(f"✅ Successfully created {len(repurposed_content)} repurposed content pieces!")
|
||||
|
||||
for i, content in enumerate(repurposed_content):
|
||||
with st.expander(f"📱 {content.platforms[0].name.title()} - {content.title}"):
|
||||
st.markdown(f"**Platform:** {content.platforms[0].name.title()}")
|
||||
st.markdown(f"**Content Type:** {content.content_type.name.replace('_', ' ').title()}")
|
||||
st.markdown(f"**Scheduled for:** {content.publish_date.strftime('%Y-%m-%d')}")
|
||||
|
||||
st.markdown("**Content:**")
|
||||
st.write(content.description)
|
||||
|
||||
if content.tags:
|
||||
st.markdown(f"**Tags:** {', '.join(content.tags)}")
|
||||
|
||||
# Action buttons
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
if st.button(f"📝 Edit", key=f"edit_{i}"):
|
||||
st.info("Edit functionality coming soon!")
|
||||
with col2:
|
||||
if st.button(f"📅 Schedule", key=f"schedule_{i}"):
|
||||
st.info("Scheduling functionality coming soon!")
|
||||
with col3:
|
||||
if st.button(f"📋 Copy", key=f"copy_{i}"):
|
||||
st.code(content.description)
|
||||
|
||||
def _display_content_series(self, series_content: Dict[str, List[ContentItem]]):
|
||||
"""Display the content series results."""
|
||||
total_pieces = sum(len(pieces) for pieces in series_content.values())
|
||||
st.success(f"✅ Successfully created content series with {total_pieces} pieces across {len(series_content)} platforms!")
|
||||
|
||||
for platform, content_pieces in series_content.items():
|
||||
st.markdown(f"### 📱 {platform.title()} Series ({len(content_pieces)} pieces)")
|
||||
|
||||
for i, content in enumerate(content_pieces):
|
||||
with st.expander(f"Part {i+1}: {content.title}"):
|
||||
st.markdown(f"**Scheduled for:** {content.publish_date.strftime('%Y-%m-%d')}")
|
||||
st.markdown("**Content:**")
|
||||
st.write(content.description)
|
||||
|
||||
if content.tags:
|
||||
st.markdown(f"**Tags:** {', '.join(content.tags)}")
|
||||
|
||||
def _display_content_analysis(self, analysis: Dict[str, Any]):
|
||||
"""Display content analysis results."""
|
||||
st.markdown("### 📊 Content Analysis Results")
|
||||
|
||||
# Content metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
content_analysis = analysis.get('content_analysis', {})
|
||||
|
||||
with col1:
|
||||
st.metric("Word Count", content_analysis.get('word_count', 0))
|
||||
|
||||
with col2:
|
||||
richness = content_analysis.get('content_richness', 'Unknown')
|
||||
st.metric("Content Richness", richness)
|
||||
|
||||
with col3:
|
||||
potential = content_analysis.get('repurposing_potential', 'Unknown')
|
||||
st.metric("Repurposing Potential", potential)
|
||||
|
||||
# Recommendations
|
||||
st.markdown("### 💡 Recommendations")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Recommended Platforms:**")
|
||||
platforms = analysis.get('platform_suggestions', [])
|
||||
for platform in platforms:
|
||||
st.write(f"• {platform.name.title()}")
|
||||
|
||||
with col2:
|
||||
st.markdown("**Suggested Strategies:**")
|
||||
strategies = analysis.get('strategy_suggestions', [])
|
||||
for strategy in strategies:
|
||||
st.write(f"• {strategy.replace('_', ' ').title()}")
|
||||
|
||||
# Content atoms
|
||||
st.markdown("### 🔬 Content Atoms Analysis")
|
||||
|
||||
atoms = content_analysis.get('content_atoms', {})
|
||||
|
||||
for atom_type, atom_list in atoms.items():
|
||||
if atom_list:
|
||||
with st.expander(f"{atom_type.title()} ({len(atom_list)} found)"):
|
||||
for atom in atom_list:
|
||||
st.write(f"• {atom}")
|
||||
|
||||
# Estimated output
|
||||
estimated = analysis.get('estimated_output', {})
|
||||
if estimated:
|
||||
st.markdown("### 📈 Estimated Output")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric("Total Pieces", estimated.get('total_pieces', 0))
|
||||
|
||||
with col2:
|
||||
st.metric("Time Savings", estimated.get('time_savings', '0 hours'))
|
||||
|
||||
with col3:
|
||||
st.metric("Content Multiplication", estimated.get('content_multiplication', '1x'))
|
||||
|
||||
def _create_series_timeline_preview(self, content: ContentItem, platforms: List[Platform]) -> pd.DataFrame:
|
||||
"""Create a preview timeline for content series."""
|
||||
timeline_data = []
|
||||
base_date = datetime.now()
|
||||
|
||||
for i, platform in enumerate(platforms):
|
||||
release_date = base_date + timedelta(days=i)
|
||||
timeline_data.append({
|
||||
'Platform': platform.name.title(),
|
||||
'Release Date': release_date.strftime('%Y-%m-%d'),
|
||||
'Content Type': self._get_platform_content_type(platform),
|
||||
'Strategy': self._get_platform_strategy(platform)
|
||||
})
|
||||
|
||||
return pd.DataFrame(timeline_data)
|
||||
|
||||
def _get_platform_content_type(self, platform: Platform) -> str:
|
||||
"""Get content type for platform."""
|
||||
types = {
|
||||
Platform.TWITTER: "Thread/Tweet",
|
||||
Platform.LINKEDIN: "Professional Post",
|
||||
Platform.INSTAGRAM: "Visual Post",
|
||||
Platform.FACEBOOK: "Engaging Post",
|
||||
Platform.WEBSITE: "Blog Article"
|
||||
}
|
||||
return types.get(platform, "Standard Post")
|
||||
|
||||
def _get_platform_strategy(self, platform: Platform) -> str:
|
||||
"""Get strategy for platform."""
|
||||
strategies = {
|
||||
Platform.TWITTER: "Hook & Engage",
|
||||
Platform.LINKEDIN: "Authority Building",
|
||||
Platform.INSTAGRAM: "Visual Storytelling",
|
||||
Platform.FACEBOOK: "Community Discussion",
|
||||
Platform.WEBSITE: "Complete Information"
|
||||
}
|
||||
return strategies.get(platform, "Standard Approach")
|
||||
|
||||
# Main function to render the UI
|
||||
def render_content_repurposing_ui():
|
||||
"""Main function to render the content repurposing UI."""
|
||||
ui = ContentRepurposingUI()
|
||||
ui.render_repurposing_interface()
|
||||
|
||||
# For testing
|
||||
if __name__ == "__main__":
|
||||
render_content_repurposing_ui()
|
||||
457
lib/content_calendar/ui/components/content_series.py
Normal file
457
lib/content_calendar/ui/components/content_series.py
Normal file
@@ -0,0 +1,457 @@
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
from lib.ai_seo_tools.content_calendar.core.content_generator import ContentGenerator
|
||||
from lib.ai_seo_tools.content_calendar.core.ai_generator import AIGenerator
|
||||
from lib.ai_seo_tools.content_calendar.integrations.seo_optimizer import SEOOptimizer
|
||||
from lib.database.models import ContentItem, ContentType, Platform, SEOData
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('content_calendar.series')
|
||||
|
||||
class SeriesManager:
|
||||
def __init__(self):
|
||||
self.series_data = {}
|
||||
if 'content_series' not in st.session_state:
|
||||
st.session_state.content_series = {}
|
||||
if 'series_relationships' not in st.session_state:
|
||||
st.session_state.series_relationships = {}
|
||||
if 'series_performance' not in st.session_state:
|
||||
st.session_state.series_performance = {}
|
||||
|
||||
def create_series(self, series_id: str, topic: str, num_pieces: int, content_type: ContentType,
|
||||
platforms: List[Platform], schedule_strategy: str = 'linear', series_type: str = '', series_flow: str = '', metadata: Dict[str, Any] = {}) -> Dict[str, Any]:
|
||||
"""Create a new content series with tracking and scheduling."""
|
||||
try:
|
||||
series = {
|
||||
'id': series_id,
|
||||
'topic': topic,
|
||||
'num_pieces': num_pieces,
|
||||
'content_type': content_type,
|
||||
'platforms': platforms,
|
||||
'schedule_strategy': schedule_strategy,
|
||||
'series_type': series_type,
|
||||
'series_flow': series_flow,
|
||||
'pieces': [],
|
||||
'performance': {},
|
||||
'created_at': datetime.now(),
|
||||
'status': 'draft',
|
||||
'relationships': {},
|
||||
'platform_distribution': {p.name: [] for p in platforms},
|
||||
'metadata': metadata
|
||||
}
|
||||
st.session_state.content_series[series_id] = series
|
||||
return series
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating series: {str(e)}")
|
||||
return None
|
||||
|
||||
def add_piece(self, series_id: str, piece: Dict[str, Any]) -> bool:
|
||||
"""Add a content piece to the series with relationship tracking."""
|
||||
try:
|
||||
if series_id in st.session_state.content_series:
|
||||
series = st.session_state.content_series[series_id]
|
||||
piece_id = f"piece_{len(series['pieces'])}"
|
||||
|
||||
# Create a structured piece object
|
||||
structured_piece = {
|
||||
'id': piece_id,
|
||||
'title': piece.get('title', f"Part {len(series['pieces']) + 1}"),
|
||||
'content': piece.get('content', ''),
|
||||
'platform': piece.get('platform', series['platforms'][0]),
|
||||
'scheduled_date': None,
|
||||
'status': 'draft',
|
||||
'relationships': {
|
||||
'previous': None,
|
||||
'next': None
|
||||
},
|
||||
'performance': {
|
||||
'engagement': 0,
|
||||
'reach': 0,
|
||||
'conversion_rate': 0
|
||||
}
|
||||
}
|
||||
|
||||
# Track relationships
|
||||
if series['pieces']:
|
||||
previous_piece = series['pieces'][-1]
|
||||
structured_piece['relationships']['previous'] = previous_piece['id']
|
||||
structured_piece['relationships']['next'] = piece_id
|
||||
|
||||
# Add to platform distribution
|
||||
platform_name = structured_piece['platform'].name
|
||||
if platform_name in series['platform_distribution']:
|
||||
series['platform_distribution'][platform_name].append(piece_id)
|
||||
|
||||
series['pieces'].append(structured_piece)
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding piece to series: {str(e)}")
|
||||
return False
|
||||
|
||||
def get_series_performance(self, series_id: str) -> Dict[str, Any]:
|
||||
"""Get comprehensive performance analytics for a series."""
|
||||
try:
|
||||
if series_id in st.session_state.content_series:
|
||||
series = st.session_state.content_series[series_id]
|
||||
performance = {
|
||||
'overall': {
|
||||
'total_engagement': 0,
|
||||
'total_reach': 0,
|
||||
'conversion_rate': 0,
|
||||
'average_engagement': 0
|
||||
},
|
||||
'platforms': {},
|
||||
'pieces': {},
|
||||
'trends': {
|
||||
'engagement': [],
|
||||
'reach': [],
|
||||
'conversions': []
|
||||
}
|
||||
}
|
||||
|
||||
# Calculate overall metrics
|
||||
for piece in series['pieces']:
|
||||
piece_performance = piece.get('performance', {})
|
||||
performance['overall']['total_engagement'] += piece_performance.get('engagement', 0)
|
||||
performance['overall']['total_reach'] += piece_performance.get('reach', 0)
|
||||
performance['overall']['conversion_rate'] += piece_performance.get('conversion_rate', 0)
|
||||
|
||||
# Track piece-specific performance
|
||||
performance['pieces'][piece['id']] = piece_performance
|
||||
|
||||
# Track trends
|
||||
performance['trends']['engagement'].append(piece_performance.get('engagement', 0))
|
||||
performance['trends']['reach'].append(piece_performance.get('reach', 0))
|
||||
performance['trends']['conversions'].append(piece_performance.get('conversion_rate', 0))
|
||||
|
||||
# Calculate averages
|
||||
num_pieces = len(series['pieces'])
|
||||
if num_pieces > 0:
|
||||
performance['overall']['average_engagement'] = performance['overall']['total_engagement'] / num_pieces
|
||||
performance['overall']['conversion_rate'] = performance['overall']['conversion_rate'] / num_pieces
|
||||
|
||||
# Calculate platform-specific performance
|
||||
for platform in series['platforms']:
|
||||
platform_pieces = series['platform_distribution'].get(platform.name, [])
|
||||
platform_performance = {
|
||||
'engagement': 0,
|
||||
'reach': 0,
|
||||
'conversion_rate': 0
|
||||
}
|
||||
|
||||
for piece_id in platform_pieces:
|
||||
piece_performance = performance['pieces'].get(piece_id, {})
|
||||
platform_performance['engagement'] += piece_performance.get('engagement', 0)
|
||||
platform_performance['reach'] += piece_performance.get('reach', 0)
|
||||
platform_performance['conversion_rate'] += piece_performance.get('conversion_rate', 0)
|
||||
|
||||
if platform_pieces:
|
||||
platform_performance['engagement'] /= len(platform_pieces)
|
||||
platform_performance['conversion_rate'] /= len(platform_pieces)
|
||||
|
||||
performance['platforms'][platform.name] = platform_performance
|
||||
|
||||
return performance
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting series performance: {str(e)}")
|
||||
return {}
|
||||
|
||||
def update_series_status(self, series_id: str, status: str) -> bool:
|
||||
"""Update the status of a series."""
|
||||
try:
|
||||
if series_id in st.session_state.content_series:
|
||||
st.session_state.content_series[series_id]['status'] = status
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating series status: {str(e)}")
|
||||
return False
|
||||
|
||||
def schedule_series(self, series_id: str, start_date: datetime, interval: int = 7) -> bool:
|
||||
"""Schedule the series content with flexible scheduling strategies."""
|
||||
try:
|
||||
if series_id in st.session_state.content_series:
|
||||
series = st.session_state.content_series[series_id]
|
||||
current_date = start_date
|
||||
|
||||
for piece in series['pieces']:
|
||||
piece['scheduled_date'] = current_date
|
||||
if series['schedule_strategy'] == 'linear':
|
||||
current_date += timedelta(days=interval)
|
||||
elif series['schedule_strategy'] == 'burst':
|
||||
current_date += timedelta(days=1)
|
||||
elif series['schedule_strategy'] == 'custom':
|
||||
# Custom scheduling is handled by the UI
|
||||
pass
|
||||
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error scheduling series: {str(e)}")
|
||||
return False
|
||||
|
||||
def render_content_series_generator(
|
||||
ai_generator: AIGenerator,
|
||||
content_generator: ContentGenerator,
|
||||
seo_optimizer: SEOOptimizer
|
||||
):
|
||||
"""Render the content series generator interface."""
|
||||
st.header("Content Series Generator")
|
||||
|
||||
# Check if calendar manager is available
|
||||
if 'calendar_manager' not in st.session_state:
|
||||
st.error("Calendar manager not initialized. Please refresh the page.")
|
||||
return
|
||||
|
||||
# Get available content
|
||||
try:
|
||||
available_content = st.session_state.calendar_manager.get_calendar().get_all_content()
|
||||
content_options = [item.title for item in available_content]
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting content options: {str(e)}")
|
||||
st.error("Error loading content. Please try again.")
|
||||
return
|
||||
|
||||
if not content_options:
|
||||
st.info("""
|
||||
## Welcome to Content Series Generator! 📚
|
||||
|
||||
Create and manage content series across multiple platforms. Here's what you can do:
|
||||
|
||||
### Features:
|
||||
- 📝 **Series Creation**: Generate connected content pieces
|
||||
- 🔄 **Cross-Platform Distribution**: Optimize for different platforms
|
||||
- 📊 **Series Analytics**: Track performance across the series
|
||||
- 📅 **Smart Scheduling**: Plan content distribution
|
||||
|
||||
### Getting Started:
|
||||
1. First, add some content to your calendar
|
||||
2. Select a topic for your content series
|
||||
3. Configure series parameters and platforms
|
||||
4. Generate and schedule your series
|
||||
|
||||
Ready to get started? Add some content to your calendar first!
|
||||
""")
|
||||
return
|
||||
|
||||
# Series Configuration
|
||||
st.subheader("Create New Content Series")
|
||||
|
||||
# Show onboarding info if no series exist
|
||||
if not st.session_state.get('content_series', {}):
|
||||
st.info("""
|
||||
### Content Series Guide
|
||||
|
||||
Create engaging content series with these features:
|
||||
|
||||
- **Series Planning**: Define your series structure and goals
|
||||
- **Content Generation**: Create connected content pieces
|
||||
- **Platform Optimization**: Adapt content for each platform
|
||||
- **Performance Tracking**: Monitor series success
|
||||
|
||||
Fill out the form below to create your first series!
|
||||
""")
|
||||
|
||||
# Initialize series manager
|
||||
series_manager = SeriesManager()
|
||||
|
||||
# Series Creation Form
|
||||
with st.form("series_creation_form"):
|
||||
st.subheader("Create New Series")
|
||||
series_topic = st.text_input("Series Topic")
|
||||
num_pieces = st.slider("Number of pieces", 2, 10, 3)
|
||||
content_type = st.selectbox(
|
||||
"Content Type",
|
||||
options=[ct.name for ct in ContentType],
|
||||
key="series_content_type"
|
||||
)
|
||||
|
||||
# Multi-platform selection
|
||||
platforms = st.multiselect(
|
||||
"Target Platforms",
|
||||
options=[p.name for p in Platform],
|
||||
default=['WEBSITE'],
|
||||
key="series_platforms"
|
||||
)
|
||||
|
||||
# Schedule strategy
|
||||
schedule_strategy = st.selectbox(
|
||||
"Schedule Strategy",
|
||||
options=['linear', 'burst', 'custom'],
|
||||
help="Linear: Evenly spaced, Burst: Grouped together, Custom: Manual scheduling"
|
||||
)
|
||||
|
||||
# Series metadata
|
||||
with st.expander("Series Metadata"):
|
||||
target_audience = st.text_area("Target Audience")
|
||||
series_goals = st.multiselect(
|
||||
"Series Goals",
|
||||
options=['Awareness', 'Engagement', 'Conversion', 'Education'],
|
||||
default=['Awareness']
|
||||
)
|
||||
series_tone = st.select_slider(
|
||||
"Series Tone",
|
||||
options=['Professional', 'Casual', 'Friendly', 'Authoritative', 'Conversational'],
|
||||
value='Professional'
|
||||
)
|
||||
|
||||
submitted = st.form_submit_button("Generate Series")
|
||||
|
||||
if submitted and series_topic:
|
||||
with st.spinner("Generating content series..."):
|
||||
try:
|
||||
# Create series
|
||||
series_id = f"series_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
|
||||
# Prepare metadata with default values
|
||||
metadata = {
|
||||
'tone': series_tone,
|
||||
'length': 'medium', # Default length
|
||||
'engagement_goal': series_goals[0] if series_goals else 'Awareness',
|
||||
'creativity_level': 'balanced' # Default creativity level
|
||||
}
|
||||
|
||||
series = series_manager.create_series(
|
||||
series_id=series_id,
|
||||
topic=series_topic,
|
||||
num_pieces=num_pieces,
|
||||
content_type=ContentType[content_type],
|
||||
platforms=[Platform[p] for p in platforms],
|
||||
schedule_strategy=schedule_strategy,
|
||||
series_type=series_goals[0] if series_goals else 'Awareness',
|
||||
series_flow='sequential', # Default flow
|
||||
metadata=metadata
|
||||
)
|
||||
|
||||
if series:
|
||||
# Generate series content
|
||||
series_content = content_generator.generate_content(
|
||||
content_type=ContentType[content_type],
|
||||
topic=series_topic,
|
||||
platforms=[Platform[p] for p in platforms],
|
||||
num_pieces=num_pieces,
|
||||
requirements={
|
||||
'tone': series_tone,
|
||||
'length': metadata['length'],
|
||||
'engagement_goal': metadata['engagement_goal'],
|
||||
'creativity_level': metadata['creativity_level'],
|
||||
'series_type': metadata['engagement_goal'],
|
||||
'series_flow': 'sequential',
|
||||
'target_audience': target_audience
|
||||
}
|
||||
)
|
||||
|
||||
if series_content:
|
||||
# Add content pieces to series
|
||||
for piece in series_content:
|
||||
series_manager.add_piece(
|
||||
series_id=series['id'],
|
||||
piece=piece
|
||||
)
|
||||
|
||||
# Schedule series
|
||||
if schedule_strategy == 'linear':
|
||||
start_date = st.date_input("Start Date", datetime.now())
|
||||
interval = st.number_input("Days between pieces", min_value=1, value=7)
|
||||
series_manager.schedule_series(
|
||||
series_id=series['id'],
|
||||
start_date=start_date,
|
||||
interval_days=interval
|
||||
)
|
||||
elif schedule_strategy == 'burst':
|
||||
start_date = st.date_input("Start Date", datetime.now())
|
||||
burst_size = st.number_input("Burst Size", min_value=1, value=1)
|
||||
series_manager.schedule_series(
|
||||
series_id=series['id'],
|
||||
start_date=start_date,
|
||||
interval_days=1,
|
||||
burst_size=burst_size
|
||||
)
|
||||
else: # custom
|
||||
for i, piece in enumerate(series_manager.series_data[series['id']]['pieces']):
|
||||
piece['scheduled_date'] = st.date_input(
|
||||
f"Publish Date for Part {i+1}",
|
||||
datetime.now() + timedelta(days=i*7)
|
||||
)
|
||||
|
||||
if st.button("Save Schedule"):
|
||||
st.success("Series schedule saved!")
|
||||
|
||||
st.success(f"Generated {num_pieces} content pieces for series!")
|
||||
|
||||
# Display series preview
|
||||
with st.expander("Series Preview", expanded=True):
|
||||
for piece in series_manager.series_data[series_id]['pieces']:
|
||||
st.markdown(f"### Part {piece['part_number']}")
|
||||
st.json(piece['content'])
|
||||
|
||||
# Platform-specific previews
|
||||
st.markdown("#### Platform Previews")
|
||||
for platform in platforms:
|
||||
with st.expander(f"{platform} Preview"):
|
||||
st.write(piece['content'].get('platform_previews', {}).get(platform, 'No preview available'))
|
||||
|
||||
# Series performance tracking
|
||||
st.subheader("Series Performance")
|
||||
performance_data = series_manager.get_series_performance(series_id)
|
||||
if performance_data:
|
||||
st.write("### Overall Performance")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Total Engagement", f"{performance_data['overall']['total_engagement']:.1f}%")
|
||||
with col2:
|
||||
st.metric("Total Reach", f"{performance_data['overall']['total_reach']:,}")
|
||||
with col3:
|
||||
st.metric("Conversion Rate", f"{performance_data['overall']['conversion_rate']:.1f}%")
|
||||
|
||||
# Platform-specific performance
|
||||
st.write("### Platform Performance")
|
||||
for platform in platforms:
|
||||
with st.expander(f"{platform} Performance"):
|
||||
platform_data = performance_data['platforms'].get(platform, {})
|
||||
st.write(f"Engagement: {platform_data.get('engagement', 0):.1f}%")
|
||||
st.write(f"Reach: {platform_data.get('reach', 0):,}")
|
||||
st.write(f"Conversions: {platform_data.get('conversion_rate', 0):.1f}%")
|
||||
|
||||
# Performance trends
|
||||
st.write("### Performance Trends")
|
||||
trend_data = performance_data['trends']
|
||||
st.line_chart(pd.DataFrame({
|
||||
'Engagement': trend_data['engagement'],
|
||||
'Reach': trend_data['reach'],
|
||||
'Conversions': trend_data['conversions']
|
||||
}))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating series: {str(e)}", exc_info=True)
|
||||
st.error(f"Error generating series: {str(e)}")
|
||||
|
||||
# Display existing series
|
||||
if st.session_state.content_series:
|
||||
st.subheader("Existing Series")
|
||||
for series_id, series in st.session_state.content_series.items():
|
||||
with st.expander(f"Series: {series['topic']}"):
|
||||
st.write(f"Status: {series['status']}")
|
||||
st.write(f"Pieces: {len(series['pieces'])}")
|
||||
st.write(f"Created: {series['created_at']}")
|
||||
|
||||
# Series actions
|
||||
if st.button(f"View Details", key=f"view_{series_id}"):
|
||||
st.session_state.selected_series = series_id
|
||||
|
||||
if st.button(f"Delete Series", key=f"delete_{series_id}"):
|
||||
del st.session_state.content_series[series_id]
|
||||
st.rerun()
|
||||
|
||||
def on_series_complete():
|
||||
"""Handle series completion."""
|
||||
try:
|
||||
st.session_state.series_complete = True
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling series completion: {str(e)}")
|
||||
st.error("An error occurred while completing the series. Please try again.")
|
||||
81
lib/content_calendar/ui/components/performance_insights.py
Normal file
81
lib/content_calendar/ui/components/performance_insights.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import streamlit as st
|
||||
from typing import Dict, Any
|
||||
from lib.database.models import ContentItem
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def render_performance_insights(content_item: ContentItem, platform_adapter) -> None:
|
||||
"""Render performance insights for a content item."""
|
||||
try:
|
||||
logger.info(f"Rendering performance insights for: {content_item.title}")
|
||||
|
||||
# Get performance data from platform adapter
|
||||
performance_data = platform_adapter.get_content_performance(content_item)
|
||||
|
||||
if not performance_data:
|
||||
st.warning("No performance data available for this content")
|
||||
return
|
||||
|
||||
# Create metrics section
|
||||
st.subheader("Performance Metrics")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric(
|
||||
"Engagement Rate",
|
||||
f"{performance_data.get('engagement_rate', 0):.1f}%",
|
||||
f"{performance_data.get('engagement_rate_change', 0):+.1f}%"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.metric(
|
||||
"Reach",
|
||||
f"{performance_data.get('reach', 0):,}",
|
||||
f"{performance_data.get('reach_change', 0):+,}"
|
||||
)
|
||||
|
||||
with col3:
|
||||
st.metric(
|
||||
"Conversion Rate",
|
||||
f"{performance_data.get('conversion_rate', 0):.1f}%",
|
||||
f"{performance_data.get('conversion_rate_change', 0):+.1f}%"
|
||||
)
|
||||
|
||||
# Create audience insights section
|
||||
st.subheader("Audience Insights")
|
||||
audience_data = performance_data.get('audience_insights', {})
|
||||
|
||||
if audience_data:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.write("Demographics")
|
||||
st.write(f"- Age: {audience_data.get('age_range', 'N/A')}")
|
||||
st.write(f"- Gender: {audience_data.get('gender', 'N/A')}")
|
||||
st.write(f"- Location: {audience_data.get('location', 'N/A')}")
|
||||
|
||||
with col2:
|
||||
st.write("Behavior")
|
||||
st.write(f"- Peak Time: {audience_data.get('peak_time', 'N/A')}")
|
||||
st.write(f"- Device: {audience_data.get('device', 'N/A')}")
|
||||
st.write(f"- Platform: {audience_data.get('platform', 'N/A')}")
|
||||
|
||||
# Create content insights section
|
||||
st.subheader("Content Insights")
|
||||
content_insights = performance_data.get('content_insights', {})
|
||||
|
||||
if content_insights:
|
||||
st.write("Top Performing Elements")
|
||||
for element, score in content_insights.get('top_elements', {}).items():
|
||||
st.write(f"- {element}: {score}")
|
||||
|
||||
st.write("Improvement Suggestions")
|
||||
for suggestion in content_insights.get('suggestions', []):
|
||||
st.write(f"- {suggestion}")
|
||||
|
||||
logger.info(f"Performance insights rendered successfully for: {content_item.title}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rendering performance insights: {str(e)}", exc_info=True)
|
||||
st.error(f"Error rendering performance insights: {str(e)}")
|
||||
638
lib/content_calendar/ui/dashboard.py
Normal file
638
lib/content_calendar/ui/dashboard.py
Normal file
@@ -0,0 +1,638 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import sys
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from .calendar_view import render_calendar_view
|
||||
from .filters import render_filters
|
||||
from .add_content_modal import render_add_content_modal
|
||||
from .ai_suggestions_modal import render_ai_suggestions_modal
|
||||
from .components.content_optimization import render_content_optimization
|
||||
from .components.ab_testing import render_ab_testing
|
||||
from .components.content_series import render_content_series_generator
|
||||
from .components.performance_insights import render_performance_insights
|
||||
import json
|
||||
from lib.content_scheduler.ui.dashboard import run_dashboard as run_scheduler_dashboard
|
||||
|
||||
# Add parent directory to path to import existing tools
|
||||
parent_dir = str(Path(__file__).parent.parent.parent.parent)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.database.models import ContentItem, ContentType, Platform, get_engine, get_session, init_db
|
||||
from ..core.calendar_manager import CalendarManager
|
||||
from ..core.content_generator import ContentGenerator
|
||||
from ..core.ai_generator import AIGenerator
|
||||
from ..core.content_brief import ContentBriefGenerator
|
||||
from ..integrations.seo_optimizer import SEOOptimizer
|
||||
from lib.integrations.platform_adapters import PlatformAdapter, UnifiedPlatformAdapter
|
||||
|
||||
# Initialize logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize DB/session (do this once at app startup)
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
# Import content repurposing UI with error handling
|
||||
def render_smart_repurposing_tab():
|
||||
"""Render the Smart Content Repurposing tab with error handling."""
|
||||
try:
|
||||
from lib.ai_seo_tools.content_calendar.ui.components.content_repurposing_ui import render_content_repurposing_ui
|
||||
render_content_repurposing_ui()
|
||||
except ImportError as e:
|
||||
st.error(f"Smart Content Repurposing feature is not available: {str(e)}")
|
||||
st.info("Please ensure all dependencies are installed correctly.")
|
||||
except Exception as e:
|
||||
st.error(f"Error loading Smart Content Repurposing: {str(e)}")
|
||||
st.info("Please check the logs for more details.")
|
||||
|
||||
class ContentCalendarDashboard:
|
||||
"""Interactive dashboard for content calendar management."""
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.dashboard')
|
||||
self.logger.info("Initializing ContentCalendarDashboard")
|
||||
self.content_brief_generator = ContentBriefGenerator()
|
||||
self.content_generator = ContentGenerator()
|
||||
self.ai_generator = AIGenerator()
|
||||
self.platform_adapter = UnifiedPlatformAdapter()
|
||||
self.seo_optimizer = SEOOptimizer()
|
||||
# Initialize session state variables
|
||||
if 'ab_test_results' not in st.session_state:
|
||||
st.session_state.ab_test_results = {}
|
||||
if 'optimization_history' not in st.session_state:
|
||||
st.session_state.optimization_history = {}
|
||||
if 'calendar_data' not in st.session_state:
|
||||
st.session_state.calendar_data = None
|
||||
if 'selected_content' not in st.session_state:
|
||||
st.session_state.selected_content = None
|
||||
if 'view_mode' not in st.session_state:
|
||||
st.session_state.view_mode = 'day'
|
||||
if 'selected_date' not in st.session_state:
|
||||
st.session_state.selected_date = datetime.now()
|
||||
self.logger.info("ContentCalendarDashboard initialized successfully")
|
||||
|
||||
def render(self):
|
||||
self.logger.info("Starting dashboard render (tabbed UI)")
|
||||
try:
|
||||
self._inject_custom_css()
|
||||
st.title("AI Content Planning")
|
||||
st.markdown("""
|
||||
Plan, schedule, and manage your content strategy with AI-powered insights. Use the calendar to organize your content and leverage AI tools for optimization.
|
||||
""")
|
||||
tabs = st.tabs([
|
||||
"Content Planning",
|
||||
"Content Optimization",
|
||||
"🔄 Smart Repurposing",
|
||||
"A/B Testing",
|
||||
"Content Series",
|
||||
"Analytics",
|
||||
"Content Scheduling"
|
||||
])
|
||||
with tabs[0]:
|
||||
icon_map = {
|
||||
'Blog': '📝', 'Website': '🌐', 'Instagram': '📸', 'Twitter': '🐦', 'LinkedIn': '💼', 'Facebook': '📘',
|
||||
'Article': '📄', 'Social Post': '💬', 'Video': '🎬', 'Newsletter': '✉️'
|
||||
}
|
||||
status_color = {
|
||||
'Draft': '#bdbdbd', 'Scheduled': '#1976d2', 'Published': '#43a047', 'Archived': '#757575'
|
||||
}
|
||||
calendar_data = self._get_calendar_data()
|
||||
def on_edit(row):
|
||||
try:
|
||||
st.session_state.editing_content = row
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling edit action: {str(e)}")
|
||||
st.error("An error occurred while editing content. Please try again.")
|
||||
def on_delete(row):
|
||||
try:
|
||||
self._delete_content(row)
|
||||
st.success(f"Successfully deleted content: {row['title']}")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling delete action: {str(e)}")
|
||||
st.error("An error occurred while deleting content. Please try again.")
|
||||
def on_generate(row):
|
||||
st.session_state['show_ai_modal'] = True
|
||||
st.session_state['ai_modal_topic'] = row['title']
|
||||
st.session_state['ai_modal_type'] = str(row['type'])
|
||||
st.session_state['ai_modal_platform'] = str(row['platform'])
|
||||
st.rerun()
|
||||
render_calendar_view(
|
||||
calendar_data=calendar_data,
|
||||
icon_map=icon_map,
|
||||
status_color=status_color,
|
||||
on_edit=on_edit,
|
||||
on_delete=on_delete,
|
||||
on_generate=on_generate,
|
||||
get_item_key=self._get_item_key
|
||||
)
|
||||
st.markdown("---")
|
||||
render_filters()
|
||||
def handle_add_content(title, platform, content_type, publish_date):
|
||||
self._add_content({
|
||||
'title': title,
|
||||
'platform': platform,
|
||||
'type': content_type,
|
||||
'publish_date': publish_date
|
||||
})
|
||||
st.session_state['show_add_content_dialog'] = False
|
||||
st.success("Content added!")
|
||||
st.rerun()
|
||||
def handle_generate_with_ai(title, platform, content_type):
|
||||
st.session_state['show_add_content_dialog'] = False
|
||||
st.session_state['show_ai_modal'] = True
|
||||
st.session_state['ai_modal_topic'] = title
|
||||
st.session_state['ai_modal_type'] = content_type
|
||||
st.session_state['ai_modal_platform'] = platform
|
||||
render_add_content_modal(
|
||||
selected_date=st.session_state.selected_date,
|
||||
on_add_content=handle_add_content,
|
||||
on_generate_with_ai=handle_generate_with_ai
|
||||
)
|
||||
if st.session_state.get('show_ai_modal', False):
|
||||
st.markdown("### AI Content Suggestions")
|
||||
with st.container():
|
||||
render_ai_suggestions_modal(
|
||||
generate_ai_suggestions=self._generate_ai_suggestions,
|
||||
on_create_brief=self._create_content_brief,
|
||||
on_schedule=self._schedule_content,
|
||||
on_refine=self._refine_suggestion,
|
||||
on_customize=self._customize_suggestion
|
||||
)
|
||||
if st.button("Close"):
|
||||
st.session_state['show_ai_modal'] = False
|
||||
with tabs[1]:
|
||||
render_content_optimization(
|
||||
content_generator=self.content_generator,
|
||||
ai_generator=self.ai_generator,
|
||||
seo_optimizer=self.seo_optimizer
|
||||
)
|
||||
with tabs[2]:
|
||||
render_smart_repurposing_tab()
|
||||
with tabs[3]:
|
||||
render_ab_testing(self.content_generator, None)
|
||||
with tabs[4]:
|
||||
render_content_series_generator(
|
||||
self.ai_generator,
|
||||
self.content_generator,
|
||||
self.seo_optimizer
|
||||
)
|
||||
with tabs[5]:
|
||||
st.header("Analytics")
|
||||
st.markdown("### Performance Insights")
|
||||
all_content = session.query(ContentItem).all()
|
||||
selected_content = st.selectbox(
|
||||
"Select content to analyze",
|
||||
options=[item.title for item in all_content],
|
||||
key="analytics_content_select"
|
||||
)
|
||||
if selected_content:
|
||||
content_item = next(
|
||||
item for item in all_content
|
||||
if item.title == selected_content
|
||||
)
|
||||
render_performance_insights(content_item, self.platform_adapter)
|
||||
st.markdown("### Optimization History")
|
||||
if selected_content in st.session_state.optimization_history:
|
||||
st.json(st.session_state.optimization_history[selected_content])
|
||||
with tabs[6]:
|
||||
run_scheduler_dashboard()
|
||||
self.logger.info("Dashboard render completed successfully (tabbed UI)")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error rendering dashboard: {str(e)}", exc_info=True)
|
||||
st.error(f"An error occurred: {str(e)}")
|
||||
|
||||
def _inject_custom_css(self):
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Add your custom CSS here if needed */
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def _get_calendar_data(self):
|
||||
self.logger.info("_get_calendar_data called")
|
||||
try:
|
||||
all_content = session.query(ContentItem).all()
|
||||
data = []
|
||||
for item in all_content:
|
||||
data.append({
|
||||
'date': item.publish_date,
|
||||
'title': item.title,
|
||||
'platform': item.platforms[0] if item.platforms else 'Unknown',
|
||||
'type': item.content_type.value if hasattr(item.content_type, 'value') else str(item.content_type),
|
||||
'status': item.status
|
||||
})
|
||||
df = pd.DataFrame(data) if data else None
|
||||
return df
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading calendar data: {str(e)}", exc_info=True)
|
||||
st.error(f"Error loading calendar data: {str(e)}")
|
||||
return None
|
||||
|
||||
def _add_content(self, content):
|
||||
platform_map = {
|
||||
'Blog': Platform.WEBSITE,
|
||||
'Instagram': Platform.INSTAGRAM,
|
||||
'Twitter': Platform.TWITTER,
|
||||
'LinkedIn': Platform.LINKEDIN,
|
||||
'Facebook': Platform.FACEBOOK,
|
||||
}
|
||||
platform_enum = platform_map.get(content['platform'], Platform.WEBSITE)
|
||||
content_type_map = {
|
||||
'Article': ContentType.BLOG_POST,
|
||||
'Social Post': ContentType.SOCIAL_MEDIA,
|
||||
'Video': ContentType.VIDEO,
|
||||
'Newsletter': ContentType.NEWSLETTER,
|
||||
}
|
||||
content_type_enum = content_type_map.get(content['type'], ContentType.BLOG_POST)
|
||||
new_item = ContentItem(
|
||||
title=content['title'],
|
||||
description="",
|
||||
content_type=content_type_enum,
|
||||
platforms=[platform_enum.value],
|
||||
publish_date=pd.to_datetime(content['publish_date']),
|
||||
status=content.get('status', 'Draft'),
|
||||
author=None,
|
||||
tags=[],
|
||||
notes=None,
|
||||
seo_data={}
|
||||
)
|
||||
session.add(new_item)
|
||||
session.commit()
|
||||
|
||||
def _delete_content(self, row):
|
||||
# Find by title and publish_date (could be improved with unique IDs)
|
||||
all_content = session.query(ContentItem).all()
|
||||
for item in all_content:
|
||||
if (item.title == row['title'] and
|
||||
str(item.publish_date.date()) == str(row['date'].date()) and
|
||||
(item.platforms[0] if item.platforms else 'Unknown') == str(row['platform']) and
|
||||
(item.content_type.value if hasattr(item.content_type, 'value') else str(item.content_type)) == str(row['type'])):
|
||||
session.delete(item)
|
||||
session.commit()
|
||||
break
|
||||
|
||||
def _edit_content(self, row, new_title, new_platform, new_type, new_status):
|
||||
self._delete_content(row)
|
||||
self._add_content({
|
||||
'title': new_title,
|
||||
'platform': new_platform,
|
||||
'type': new_type,
|
||||
'publish_date': row['date'],
|
||||
'status': new_status
|
||||
})
|
||||
|
||||
def _get_item_key(self, row):
|
||||
key_str = f"{row['title']}_{row['date']}_{row['platform']}_{row['type']}"
|
||||
return hashlib.md5(key_str.encode()).hexdigest()
|
||||
|
||||
def _generate_ai_suggestions(self, content_type, topic, audience, goals, tone, length, model_settings, style_preferences, seo_preferences, platform_settings):
|
||||
"""Generate AI content suggestions based on input parameters."""
|
||||
try:
|
||||
self.logger.info(f"Generating AI suggestions for topic: {topic}")
|
||||
|
||||
# Map content type string to ContentType enum
|
||||
content_type_map = {
|
||||
'Blog Post': ContentType.BLOG_POST,
|
||||
'Social Media Post': ContentType.SOCIAL_MEDIA,
|
||||
'Video': ContentType.VIDEO,
|
||||
'Newsletter': ContentType.NEWSLETTER,
|
||||
'Article': ContentType.BLOG_POST,
|
||||
'Social Post': ContentType.SOCIAL_MEDIA
|
||||
}
|
||||
content_type_enum = content_type_map.get(content_type, ContentType.BLOG_POST)
|
||||
|
||||
# Map platform string to Platform enum
|
||||
platform_map = {
|
||||
'Blog': Platform.WEBSITE,
|
||||
'Instagram': Platform.INSTAGRAM,
|
||||
'Twitter': Platform.TWITTER,
|
||||
'LinkedIn': Platform.LINKEDIN,
|
||||
'Facebook': Platform.FACEBOOK,
|
||||
'Website': Platform.WEBSITE
|
||||
}
|
||||
platform = st.session_state.get('ai_modal_platform', 'Blog')
|
||||
platform_enum = platform_map.get(platform, Platform.WEBSITE)
|
||||
|
||||
# Create a content item for the suggestion
|
||||
content_item = ContentItem(
|
||||
title=topic,
|
||||
description="",
|
||||
content_type=content_type_enum,
|
||||
platforms=[platform_enum],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=topic,
|
||||
meta_description="",
|
||||
keywords=[],
|
||||
structured_data={}
|
||||
),
|
||||
status='Draft'
|
||||
)
|
||||
|
||||
# Use AIGenerator to generate suggestions
|
||||
suggestions = self.ai_generator.generate_ai_suggestions(
|
||||
content_type=content_type_enum,
|
||||
topic=topic,
|
||||
audience=audience,
|
||||
goals=goals,
|
||||
tone=tone,
|
||||
length=length,
|
||||
model_settings=model_settings,
|
||||
style_preferences=style_preferences,
|
||||
seo_preferences=seo_preferences,
|
||||
platform_settings=platform_settings,
|
||||
platform=platform_enum
|
||||
)
|
||||
|
||||
if not suggestions:
|
||||
self.logger.warning("No suggestions generated")
|
||||
return []
|
||||
|
||||
# Format suggestions
|
||||
formatted_suggestions = []
|
||||
for suggestion in suggestions:
|
||||
formatted_suggestion = {
|
||||
'title': suggestion.get('title', topic),
|
||||
'type': content_type,
|
||||
'platform': platform,
|
||||
'audience': audience,
|
||||
'impact': f"High impact for {', '.join(goals)}",
|
||||
'preview': suggestion.get('preview', ''),
|
||||
'style_elements': [
|
||||
f"Tone: {tone}",
|
||||
f"Length: {length}",
|
||||
f"Creativity: {model_settings.get('Creativity Level', 'balanced')}",
|
||||
f"Formality: {model_settings.get('Formality Level', 'professional')}"
|
||||
],
|
||||
'seo_elements': [
|
||||
f"Keyword Density: {seo_preferences.get('Keyword Density', '2')}%",
|
||||
"Internal Linking: Enabled" if seo_preferences.get('Internal Linking', True) else "Internal Linking: Disabled",
|
||||
"External Linking: Enabled" if seo_preferences.get('External Linking', True) else "External Linking: Disabled"
|
||||
],
|
||||
'engagement_score': f"{85 + len(formatted_suggestions)*5}%",
|
||||
'reach': 'High',
|
||||
'conversion': f"{3.5 + len(formatted_suggestions)*0.5}%",
|
||||
'seo_impact': 'Strong',
|
||||
'platform_optimizations': suggestion.get('platform_optimizations', []),
|
||||
'variations': suggestion.get('variations', [
|
||||
"Alternative headline",
|
||||
"Different content angle",
|
||||
"Alternative format"
|
||||
]),
|
||||
'seo_recommendations': suggestion.get('seo_elements', []),
|
||||
'media_suggestions': suggestion.get('media_suggestions', [
|
||||
"Featured image",
|
||||
"Supporting graphics",
|
||||
"Social media visuals"
|
||||
])
|
||||
}
|
||||
formatted_suggestions.append(formatted_suggestion)
|
||||
|
||||
self.logger.info(f"Generated {len(formatted_suggestions)} suggestions successfully")
|
||||
return formatted_suggestions
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating AI suggestions: {str(e)}", exc_info=True)
|
||||
st.error(f"Error generating suggestions: {str(e)}")
|
||||
return []
|
||||
|
||||
def _create_content_brief(self, content_item: ContentItem) -> Dict[str, Any]:
|
||||
"""Create a detailed content brief for the given content item."""
|
||||
try:
|
||||
self.logger.info(f"Creating content brief for: {content_item.title}")
|
||||
|
||||
# Generate content brief using the content brief generator
|
||||
brief = self.content_brief_generator.generate_brief(
|
||||
content_item=content_item,
|
||||
target_audience={
|
||||
'audience': content_item.description,
|
||||
'goals': ['engage', 'inform', 'convert']
|
||||
}
|
||||
)
|
||||
|
||||
# Enhance brief with SEO data
|
||||
if brief and 'content_flow' in brief:
|
||||
brief['seo_optimization'] = {
|
||||
'meta_description': self.seo_optimizer.generate_meta_description(
|
||||
brief['content_flow'].get('introduction', {}).get('summary', '')
|
||||
),
|
||||
'keywords': self.seo_optimizer.extract_keywords(
|
||||
brief['content_flow'].get('introduction', {}).get('summary', '')
|
||||
),
|
||||
'structured_data': self.seo_optimizer.generate_structured_data(
|
||||
content_item.content_type
|
||||
)
|
||||
}
|
||||
|
||||
self.logger.info(f"Content brief created successfully for: {content_item.title}")
|
||||
return brief
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating content brief: {str(e)}", exc_info=True)
|
||||
st.error(f"Error creating content brief: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _schedule_content(self, content_item: ContentItem, publish_date: datetime) -> bool:
|
||||
"""Schedule content for publishing on the specified date."""
|
||||
try:
|
||||
self.logger.info(f"Scheduling content: {content_item.title} for {publish_date}")
|
||||
|
||||
# Get the calendar
|
||||
calendar = self.calendar_manager.get_calendar()
|
||||
if not calendar:
|
||||
raise ValueError("No calendar found")
|
||||
|
||||
# Update the publish date
|
||||
content_item.publish_date = publish_date
|
||||
|
||||
# Add to calendar
|
||||
calendar.add_content(content_item)
|
||||
|
||||
# Save changes
|
||||
self.calendar_manager.save_calendar_to_json()
|
||||
|
||||
self.logger.info(f"Content scheduled successfully: {content_item.title}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error scheduling content: {str(e)}", exc_info=True)
|
||||
st.error(f"Error scheduling content: {str(e)}")
|
||||
return False
|
||||
|
||||
def _refine_suggestion(self, suggestion: Dict[str, Any], feedback: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Refine an AI-generated suggestion based on user feedback."""
|
||||
try:
|
||||
self.logger.info("Refining AI suggestion based on feedback")
|
||||
|
||||
# Update suggestion based on feedback
|
||||
if 'tone' in feedback:
|
||||
suggestion['style_elements'] = [
|
||||
f"Tone: {feedback['tone']}",
|
||||
*[elem for elem in suggestion['style_elements'] if not elem.startswith('Tone:')]
|
||||
]
|
||||
|
||||
if 'length' in feedback:
|
||||
suggestion['style_elements'] = [
|
||||
f"Length: {feedback['length']}",
|
||||
*[elem for elem in suggestion['style_elements'] if not elem.startswith('Length:')]
|
||||
]
|
||||
|
||||
if 'keywords' in feedback:
|
||||
suggestion['seo_elements'] = [
|
||||
f"Keywords: {', '.join(feedback['keywords'])}",
|
||||
*[elem for elem in suggestion['seo_elements'] if not elem.startswith('Keywords:')]
|
||||
]
|
||||
|
||||
# Regenerate content with refined parameters
|
||||
refined_content = self.content_brief_generator.generate_brief(
|
||||
content_item=ContentItem(
|
||||
title=suggestion['title'],
|
||||
description="",
|
||||
content_type=ContentType[suggestion['type'].upper().replace(' ', '_')],
|
||||
platforms=[Platform[suggestion['platform'].upper()]],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=suggestion['title'],
|
||||
meta_description="",
|
||||
keywords=feedback.get('keywords', []),
|
||||
structured_data={}
|
||||
),
|
||||
status='Draft'
|
||||
),
|
||||
target_audience={
|
||||
'audience': suggestion['audience'],
|
||||
'goals': feedback.get('goals', ['engage', 'inform']),
|
||||
'preferences': {
|
||||
'tone': feedback.get('tone', 'professional'),
|
||||
'length': feedback.get('length', 'medium')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if refined_content:
|
||||
suggestion['preview'] = refined_content.get('content_flow', {}).get('introduction', {}).get('summary', '')
|
||||
|
||||
self.logger.info("Suggestion refined successfully")
|
||||
return suggestion
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error refining suggestion: {str(e)}", exc_info=True)
|
||||
st.error(f"Error refining suggestion: {str(e)}")
|
||||
return suggestion
|
||||
|
||||
def _customize_suggestion(self, suggestion: Dict[str, Any], customizations: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Customize an AI-generated suggestion with specific requirements."""
|
||||
try:
|
||||
self.logger.info("Customizing AI suggestion")
|
||||
|
||||
# Apply customizations
|
||||
if 'title' in customizations:
|
||||
suggestion['title'] = customizations['title']
|
||||
|
||||
if 'platform' in customizations:
|
||||
suggestion['platform'] = customizations['platform']
|
||||
|
||||
if 'style' in customizations:
|
||||
suggestion['style_elements'] = [
|
||||
f"Tone: {customizations['style'].get('tone', 'professional')}",
|
||||
f"Length: {customizations['style'].get('length', 'medium')}",
|
||||
f"Creativity: {customizations['style'].get('creativity', 'balanced')}",
|
||||
f"Formality: {customizations['style'].get('formality', 'professional')}"
|
||||
]
|
||||
|
||||
if 'seo' in customizations:
|
||||
suggestion['seo_elements'] = [
|
||||
f"Keyword Density: {customizations['seo'].get('keyword_density', '2')}%",
|
||||
"Internal Linking: Enabled" if customizations['seo'].get('internal_linking', True) else "Internal Linking: Disabled",
|
||||
"External Linking: Enabled" if customizations['seo'].get('external_linking', True) else "External Linking: Disabled"
|
||||
]
|
||||
|
||||
# Regenerate content with customizations
|
||||
customized_content = self.content_brief_generator.generate_brief(
|
||||
content_item=ContentItem(
|
||||
title=suggestion['title'],
|
||||
description="",
|
||||
content_type=ContentType[suggestion['type'].upper().replace(' ', '_')],
|
||||
platforms=[Platform[suggestion['platform'].upper()]],
|
||||
publish_date=datetime.now(),
|
||||
seo_data=SEOData(
|
||||
title=suggestion['title'],
|
||||
meta_description="",
|
||||
keywords=customizations.get('seo', {}).get('keywords', []),
|
||||
structured_data={}
|
||||
),
|
||||
status='Draft'
|
||||
),
|
||||
target_audience={
|
||||
'audience': suggestion['audience'],
|
||||
'goals': customizations.get('goals', ['engage', 'inform']),
|
||||
'preferences': customizations.get('style', {})
|
||||
}
|
||||
)
|
||||
|
||||
if customized_content:
|
||||
suggestion['preview'] = customized_content.get('content_flow', {}).get('introduction', {}).get('summary', '')
|
||||
|
||||
self.logger.info("Suggestion customized successfully")
|
||||
return suggestion
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error customizing suggestion: {str(e)}", exc_info=True)
|
||||
st.error(f"Error customizing suggestion: {str(e)}")
|
||||
return suggestion
|
||||
|
||||
def _optimize_content_for_platform(self, content_item: ContentItem, platform: Platform) -> Dict[str, Any]:
|
||||
"""Optimize content specifically for a target platform."""
|
||||
try:
|
||||
self.logger.info(f"Optimizing content for {platform.name}: {content_item.title}")
|
||||
|
||||
# Get platform-specific requirements
|
||||
platform_requirements = self.platform_adapter.get_platform_requirements(platform)
|
||||
|
||||
# Generate platform-optimized content
|
||||
optimized_content = self.content_generator.optimize_for_platform(
|
||||
content=content_item,
|
||||
platform=platform,
|
||||
requirements=platform_requirements
|
||||
)
|
||||
|
||||
if not optimized_content:
|
||||
raise ValueError(f"Failed to optimize content for {platform.name}")
|
||||
|
||||
# Enhance with AI
|
||||
ai_enhanced = self.ai_generator.enhance_for_platform(
|
||||
content=optimized_content,
|
||||
platform=platform,
|
||||
enhancement_type='platform_specific'
|
||||
)
|
||||
|
||||
if ai_enhanced:
|
||||
optimized_content.update(ai_enhanced)
|
||||
|
||||
# Track optimization history
|
||||
if content_item.title not in st.session_state.optimization_history:
|
||||
st.session_state.optimization_history[content_item.title] = []
|
||||
st.session_state.optimization_history[content_item.title].append({
|
||||
'platform': platform.name,
|
||||
'timestamp': datetime.now(),
|
||||
'changes': optimized_content.get('changes', [])
|
||||
})
|
||||
|
||||
self.logger.info(f"Content optimized successfully for {platform.name}")
|
||||
return optimized_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error optimizing content: {str(e)}", exc_info=True)
|
||||
st.error(f"Error optimizing content: {str(e)}")
|
||||
return {}
|
||||
|
||||
if __name__ == "__main__":
|
||||
dashboard = ContentCalendarDashboard()
|
||||
dashboard.render()
|
||||
30
lib/content_calendar/ui/filters.py
Normal file
30
lib/content_calendar/ui/filters.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import streamlit as st
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def render_filters():
|
||||
with st.expander("Filters", expanded=False):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
start_date = st.date_input("Start Date", st.session_state.get('filter_start_date', datetime.now()))
|
||||
end_date = st.date_input("End Date", st.session_state.get('filter_end_date', datetime.now() + timedelta(days=30)))
|
||||
st.session_state['filter_start_date'] = start_date
|
||||
st.session_state['filter_end_date'] = end_date
|
||||
with col2:
|
||||
platforms = st.multiselect(
|
||||
"Platforms",
|
||||
["Blog", "Instagram", "Twitter", "LinkedIn", "Facebook"],
|
||||
default=st.session_state.get('filter_platforms', ["Blog"])
|
||||
)
|
||||
st.session_state['filter_platforms'] = platforms
|
||||
content_types = st.multiselect(
|
||||
"Content Types",
|
||||
["Article", "Social Post", "Video", "Newsletter"],
|
||||
default=st.session_state.get('filter_content_types', ["Article"])
|
||||
)
|
||||
st.session_state['filter_content_types'] = content_types
|
||||
statuses = st.multiselect(
|
||||
"Status",
|
||||
["Draft", "Scheduled", "Published", "Archived"],
|
||||
default=st.session_state.get('filter_statuses', ["Draft", "Scheduled"])
|
||||
)
|
||||
st.session_state['filter_statuses'] = statuses
|
||||
Reference in New Issue
Block a user