ALwrity + Wordpress + Wix + GSC integration
This commit is contained in:
@@ -1,482 +0,0 @@
|
||||
"""
|
||||
Bootstrap AI Competitive Suite
|
||||
|
||||
All-in-one AI-powered competitive tools for solo entrepreneurs.
|
||||
Combines content performance prediction and competitive intelligence.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
# Import the AI-powered tools
|
||||
from lib.content_performance_predictor.ai_performance_predictor import AIContentPerformancePredictor
|
||||
from lib.competitive_intelligence.ai_competitive_intelligence import AICompetitiveIntelligence
|
||||
|
||||
|
||||
class BootstrapAISuite:
|
||||
"""
|
||||
Unified AI suite for bootstrapped entrepreneurs.
|
||||
Combines content performance prediction and competitive intelligence.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the bootstrap AI suite."""
|
||||
self.content_predictor = AIContentPerformancePredictor()
|
||||
self.competitor_intel = AICompetitiveIntelligence()
|
||||
|
||||
logger.info("Bootstrap AI Suite initialized")
|
||||
|
||||
def get_suite_capabilities(self) -> Dict[str, Any]:
|
||||
"""Get all suite capabilities."""
|
||||
return {
|
||||
'content_prediction': {
|
||||
'name': 'AI Content Performance Predictor',
|
||||
'description': 'Predict content performance using AI analysis',
|
||||
'features': [
|
||||
'Platform-specific optimization',
|
||||
'Engagement prediction',
|
||||
'Content recommendations',
|
||||
'Hashtag optimization',
|
||||
'Posting time suggestions'
|
||||
]
|
||||
},
|
||||
'competitive_intelligence': {
|
||||
'name': 'Bootstrap Competitive Intelligence',
|
||||
'description': 'AI-powered competitor analysis for startups',
|
||||
'features': [
|
||||
'Competitor weakness identification',
|
||||
'Content gap analysis',
|
||||
'Strategic recommendations',
|
||||
'Quick win opportunities',
|
||||
'Market positioning advice'
|
||||
]
|
||||
},
|
||||
'integrated_workflows': {
|
||||
'name': 'Smart Workflows',
|
||||
'description': 'Combined insights from both tools',
|
||||
'features': [
|
||||
'Competitor-informed content strategy',
|
||||
'Performance-optimized competitive positioning',
|
||||
'Data-driven differentiation',
|
||||
'Strategic content calendar'
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async def get_competitive_content_strategy(
|
||||
self,
|
||||
content: str,
|
||||
target_platform: str,
|
||||
competitor_urls: List[str],
|
||||
industry: str,
|
||||
your_strengths: List[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get a comprehensive content strategy combining performance prediction
|
||||
and competitive analysis.
|
||||
"""
|
||||
logger.info("Generating competitive content strategy")
|
||||
|
||||
try:
|
||||
# Step 1: Predict content performance
|
||||
st.info("🎯 Analyzing content performance potential...")
|
||||
performance_analysis = await self.content_predictor.predict_performance(
|
||||
content, target_platform
|
||||
)
|
||||
|
||||
# Step 2: Get competitive intelligence
|
||||
st.info("🕵️ Analyzing competitive landscape...")
|
||||
competitive_analysis = await self.competitor_intel.analyze_competitors(
|
||||
competitor_urls, industry, your_strengths
|
||||
)
|
||||
|
||||
# Step 3: Generate integrated strategy
|
||||
st.info("🧠 Creating integrated strategy...")
|
||||
integrated_strategy = await self._create_integrated_strategy(
|
||||
performance_analysis, competitive_analysis, content, target_platform
|
||||
)
|
||||
|
||||
return {
|
||||
'content_performance': performance_analysis,
|
||||
'competitive_intelligence': competitive_analysis,
|
||||
'integrated_strategy': integrated_strategy,
|
||||
'action_plan': self._create_action_plan(
|
||||
performance_analysis, competitive_analysis, integrated_strategy
|
||||
)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error generating competitive content strategy: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {'error': error_msg}
|
||||
|
||||
async def _create_integrated_strategy(
|
||||
self,
|
||||
performance_analysis: Dict[str, Any],
|
||||
competitive_analysis: Dict[str, Any],
|
||||
content: str,
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Create integrated strategy combining both analyses."""
|
||||
|
||||
# Extract key insights
|
||||
predicted_performance = performance_analysis.get('predictions', {})
|
||||
competitor_insights = competitive_analysis.get('competitor_insights', {})
|
||||
content_gaps = competitive_analysis.get('content_gap_analysis', {})
|
||||
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
integration_prompt = f"""
|
||||
Create an integrated content strategy that combines performance prediction and competitive analysis:
|
||||
|
||||
CONTENT TO ANALYZE:
|
||||
"{content[:500]}"
|
||||
|
||||
TARGET PLATFORM: {platform}
|
||||
|
||||
PERFORMANCE PREDICTIONS:
|
||||
{predicted_performance}
|
||||
|
||||
COMPETITIVE INSIGHTS:
|
||||
Key Insights: {competitor_insights.get('key_insights', [])}
|
||||
Content Gaps: {content_gaps.get('priority_gaps', [])}
|
||||
Quick Wins: {competitive_analysis.get('quick_wins', [])}
|
||||
|
||||
Provide an integrated strategy that answers:
|
||||
|
||||
1. CONTENT OPTIMIZATION BASED ON COMPETITION:
|
||||
- How can you modify your content to outperform competitors?
|
||||
- What competitive advantages can you highlight?
|
||||
- How can you fill content gaps while maintaining performance?
|
||||
|
||||
2. STRATEGIC POSITIONING:
|
||||
- How does your predicted performance compare to competitive landscape?
|
||||
- What unique angle can you take that competitors miss?
|
||||
- How can you leverage competitor weaknesses in your content?
|
||||
|
||||
3. PERFORMANCE ENHANCEMENT:
|
||||
- What competitive insights can improve your predicted performance?
|
||||
- Which competitor strategies should you adopt or avoid?
|
||||
- How can you differentiate while maintaining engagement?
|
||||
|
||||
4. TACTICAL EXECUTION:
|
||||
- What specific changes will maximize both performance and competitive advantage?
|
||||
- How should you sequence your content to beat competitors?
|
||||
- What metrics should you track for competitive success?
|
||||
|
||||
Provide specific, actionable recommendations for a solo entrepreneur.
|
||||
"""
|
||||
|
||||
try:
|
||||
integrated_analysis = llm_text_gen(
|
||||
integration_prompt,
|
||||
system_prompt="You are a strategic content consultant specializing in competitive content strategy for solo entrepreneurs. Combine performance optimization with competitive intelligence."
|
||||
)
|
||||
|
||||
return {
|
||||
'full_strategy': integrated_analysis,
|
||||
'optimization_recommendations': self._extract_optimization_recs(integrated_analysis),
|
||||
'competitive_positioning': self._extract_positioning_strategy(integrated_analysis),
|
||||
'performance_tactics': self._extract_performance_tactics(integrated_analysis)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating integrated strategy: {str(e)}")
|
||||
return {
|
||||
'full_strategy': f"Error generating integrated strategy: {str(e)}",
|
||||
'optimization_recommendations': [],
|
||||
'competitive_positioning': 'Focus on unique value proposition',
|
||||
'performance_tactics': []
|
||||
}
|
||||
|
||||
def _extract_optimization_recs(self, strategy: str) -> List[str]:
|
||||
"""Extract optimization recommendations."""
|
||||
recommendations = []
|
||||
|
||||
lines = strategy.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if ('optimize' in line.lower() or 'improve' in line.lower() or
|
||||
'enhance' in line.lower()) and len(line) > 20:
|
||||
recommendations.append(line.lstrip('•-* ').strip())
|
||||
|
||||
return recommendations[:5]
|
||||
|
||||
def _extract_positioning_strategy(self, strategy: str) -> str:
|
||||
"""Extract positioning strategy."""
|
||||
lines = strategy.split('\n')
|
||||
for line in lines:
|
||||
if ('position' in line.lower() or 'unique' in line.lower() or
|
||||
'differentiate' in line.lower()) and len(line) > 30:
|
||||
return line.strip()
|
||||
|
||||
return "Focus on unique value proposition that competitors can't match"
|
||||
|
||||
def _extract_performance_tactics(self, strategy: str) -> List[str]:
|
||||
"""Extract performance tactics."""
|
||||
tactics = []
|
||||
|
||||
lines = strategy.split('\n')
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if ('tactic' in line.lower() or 'execute' in line.lower() or
|
||||
'implement' in line.lower()) and len(line) > 20:
|
||||
tactics.append(line.lstrip('•-* ').strip())
|
||||
|
||||
return tactics[:4]
|
||||
|
||||
def _create_action_plan(
|
||||
self,
|
||||
performance_analysis: Dict[str, Any],
|
||||
competitive_analysis: Dict[str, Any],
|
||||
integrated_strategy: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Create actionable plan from all analyses."""
|
||||
|
||||
action_plan = []
|
||||
|
||||
# From performance analysis
|
||||
recommendations = performance_analysis.get('recommendations', [])
|
||||
for rec in recommendations[:2]:
|
||||
action_plan.append({
|
||||
'category': 'Content Performance',
|
||||
'action': rec,
|
||||
'priority': 'High',
|
||||
'timeframe': 'Immediate',
|
||||
'source': 'AI Performance Predictor'
|
||||
})
|
||||
|
||||
# From competitive analysis
|
||||
quick_wins = competitive_analysis.get('quick_wins', [])
|
||||
for win in quick_wins[:2]:
|
||||
action_plan.append({
|
||||
'category': 'Competitive Strategy',
|
||||
'action': win.get('action', win),
|
||||
'priority': 'High',
|
||||
'timeframe': win.get('timeframe', '1-2 weeks') if isinstance(win, dict) else '1-2 weeks',
|
||||
'source': 'Competitive Intelligence'
|
||||
})
|
||||
|
||||
# From integrated strategy
|
||||
optimization_recs = integrated_strategy.get('optimization_recommendations', [])
|
||||
for rec in optimization_recs[:2]:
|
||||
action_plan.append({
|
||||
'category': 'Integrated Strategy',
|
||||
'action': rec,
|
||||
'priority': 'Medium',
|
||||
'timeframe': '1-4 weeks',
|
||||
'source': 'Integrated Analysis'
|
||||
})
|
||||
|
||||
return action_plan
|
||||
|
||||
|
||||
def render_bootstrap_ai_suite():
|
||||
"""Render the complete bootstrap AI suite interface."""
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Bootstrap AI Competitive Suite",
|
||||
page_icon="🚀",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
st.title("🚀 Bootstrap AI Competitive Suite")
|
||||
st.markdown("**The complete AI toolkit for solo entrepreneurs competing against big players**")
|
||||
|
||||
# Initialize suite
|
||||
if 'ai_suite' not in st.session_state:
|
||||
st.session_state.ai_suite = BootstrapAISuite()
|
||||
|
||||
suite = st.session_state.ai_suite
|
||||
|
||||
# Sidebar with capabilities
|
||||
st.sidebar.header("🎯 AI Suite Capabilities")
|
||||
capabilities = suite.get_suite_capabilities()
|
||||
|
||||
for key, capability in capabilities.items():
|
||||
with st.sidebar.expander(capability['name']):
|
||||
st.write(capability['description'])
|
||||
for feature in capability['features']:
|
||||
st.write(f"• {feature}")
|
||||
|
||||
# Main tabs
|
||||
tab1, tab2, tab3 = st.tabs([
|
||||
"🎯 Content Performance Predictor",
|
||||
"🕵️ Competitive Intelligence",
|
||||
"🧠 Integrated Strategy"
|
||||
])
|
||||
|
||||
# Tab 1: Content Performance Predictor
|
||||
with tab1:
|
||||
st.header("🎯 AI Content Performance Predictor")
|
||||
st.markdown("Predict how well your content will perform before you publish")
|
||||
|
||||
# Import and render the AI predictor UI
|
||||
from lib.content_performance_predictor.ai_performance_predictor import render_ai_predictor_ui
|
||||
render_ai_predictor_ui()
|
||||
|
||||
# Tab 2: Competitive Intelligence
|
||||
with tab2:
|
||||
st.header("🕵️ Bootstrap Competitive Intelligence")
|
||||
st.markdown("Analyze competitors and find opportunities to outmaneuver them")
|
||||
|
||||
# Import and render the competitive intelligence UI
|
||||
from lib.competitive_intelligence.ai_competitive_intelligence import render_ai_competitive_intelligence_ui
|
||||
render_ai_competitive_intelligence_ui()
|
||||
|
||||
# Tab 3: Integrated Strategy
|
||||
with tab3:
|
||||
st.header("🧠 Integrated AI Strategy")
|
||||
st.markdown("Combine content performance prediction with competitive intelligence for maximum impact")
|
||||
|
||||
# Input section
|
||||
st.subheader("Strategy Input")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
content = st.text_area(
|
||||
"Content to Analyze",
|
||||
value="Discover how AI can revolutionize your content creation process with our innovative tools designed specifically for solo entrepreneurs and small businesses.",
|
||||
height=100,
|
||||
help="Enter the content you want to optimize"
|
||||
)
|
||||
|
||||
platform = st.selectbox(
|
||||
"Target Platform",
|
||||
["Twitter", "LinkedIn", "Facebook", "Instagram"],
|
||||
help="Which platform will you publish on?"
|
||||
)
|
||||
|
||||
with col2:
|
||||
industry = st.text_input(
|
||||
"Your Industry",
|
||||
value="AI Content Creation",
|
||||
help="What industry are you in?"
|
||||
)
|
||||
|
||||
competitor_urls = st.text_area(
|
||||
"Competitor URLs (one per line)",
|
||||
value="https://jasper.ai\nhttps://copy.ai\nhttps://writesonic.com",
|
||||
height=80,
|
||||
help="URLs of your main competitors"
|
||||
)
|
||||
|
||||
your_strengths = st.text_input(
|
||||
"Your Key Strengths (comma-separated)",
|
||||
value="Personal touch, Agility, Niche expertise",
|
||||
help="What advantages do you have?"
|
||||
)
|
||||
|
||||
# Process inputs
|
||||
urls = [url.strip() for url in competitor_urls.split('\n') if url.strip()]
|
||||
strengths = [s.strip() for s in your_strengths.split(',') if s.strip()] if your_strengths else []
|
||||
|
||||
if st.button("🚀 Generate Integrated Strategy", type="primary"):
|
||||
if content and platform and urls and industry:
|
||||
with st.spinner("🧠 AI is creating your competitive content strategy..."):
|
||||
strategy_result = asyncio.run(
|
||||
suite.get_competitive_content_strategy(
|
||||
content, platform, urls, industry, strengths
|
||||
)
|
||||
)
|
||||
|
||||
if 'error' not in strategy_result:
|
||||
st.success("✅ Integrated strategy generated!")
|
||||
|
||||
# Action Plan First (most important)
|
||||
action_plan = strategy_result.get('action_plan', [])
|
||||
if action_plan:
|
||||
st.header("📋 Your Action Plan")
|
||||
st.markdown("**Do these actions in order for maximum impact:**")
|
||||
|
||||
for i, action in enumerate(action_plan):
|
||||
with st.expander(f"Action #{i+1}: {action.get('category', 'Action')} - {action.get('priority', 'Medium')} Priority"):
|
||||
st.write(f"**What to do:** {action.get('action', 'N/A')}")
|
||||
st.write(f"**Timeframe:** {action.get('timeframe', 'N/A')}")
|
||||
st.write(f"**Source:** {action.get('source', 'N/A')}")
|
||||
|
||||
# Integrated Strategy
|
||||
integrated = strategy_result.get('integrated_strategy', {})
|
||||
if integrated:
|
||||
st.header("🧠 Integrated Strategy")
|
||||
|
||||
# Key recommendations
|
||||
optimization_recs = integrated.get('optimization_recommendations', [])
|
||||
if optimization_recs:
|
||||
st.subheader("🎯 Content Optimization")
|
||||
for rec in optimization_recs:
|
||||
st.info(f"💡 {rec}")
|
||||
|
||||
# Positioning strategy
|
||||
positioning = integrated.get('competitive_positioning', '')
|
||||
if positioning:
|
||||
st.subheader("🏆 Competitive Positioning")
|
||||
st.success(f"📍 {positioning}")
|
||||
|
||||
# Performance tactics
|
||||
tactics = integrated.get('performance_tactics', [])
|
||||
if tactics:
|
||||
st.subheader("⚡ Performance Tactics")
|
||||
for tactic in tactics:
|
||||
st.write(f"• {tactic}")
|
||||
|
||||
# Detailed analyses in expandable sections
|
||||
with st.expander("📊 Content Performance Analysis"):
|
||||
performance = strategy_result.get('content_performance', {})
|
||||
predictions = performance.get('predictions', {})
|
||||
|
||||
if predictions:
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Engagement Score", f"{predictions.get('engagement_score', 0)}/10")
|
||||
with col2:
|
||||
st.metric("Virality Potential", f"{predictions.get('virality_potential', 0)}/10")
|
||||
with col3:
|
||||
st.metric("Platform Fit", f"{predictions.get('platform_optimization', 0)}/10")
|
||||
|
||||
recommendations = performance.get('recommendations', [])
|
||||
if recommendations:
|
||||
st.write("**Performance Recommendations:**")
|
||||
for rec in recommendations:
|
||||
st.write(f"• {rec}")
|
||||
|
||||
with st.expander("🕵️ Competitive Intelligence"):
|
||||
competitive = strategy_result.get('competitive_intelligence', {})
|
||||
insights = competitive.get('competitor_insights', {})
|
||||
|
||||
key_insights = insights.get('key_insights', [])
|
||||
if key_insights:
|
||||
st.write("**Key Competitive Insights:**")
|
||||
for insight in key_insights[:3]:
|
||||
st.write(f"• {insight}")
|
||||
|
||||
quick_wins = competitive.get('quick_wins', [])
|
||||
if quick_wins:
|
||||
st.write("**Quick Competitive Wins:**")
|
||||
for win in quick_wins[:3]:
|
||||
if isinstance(win, dict):
|
||||
st.write(f"• {win.get('action', win)}")
|
||||
else:
|
||||
st.write(f"• {win}")
|
||||
|
||||
with st.expander("🤖 Complete Strategic Analysis"):
|
||||
full_strategy = integrated.get('full_strategy', 'No detailed strategy available')
|
||||
st.write(full_strategy)
|
||||
|
||||
else:
|
||||
st.error(f"❌ Strategy generation failed: {strategy_result.get('error')}")
|
||||
else:
|
||||
st.warning("⚠️ Please fill in all required fields")
|
||||
|
||||
# Footer
|
||||
st.markdown("---")
|
||||
st.markdown("**💡 Pro Tip:** Use all three tools together for maximum competitive advantage. Start with the integrated strategy for best results!")
|
||||
|
||||
|
||||
# Main execution
|
||||
if __name__ == "__main__":
|
||||
render_bootstrap_ai_suite()
|
||||
@@ -1,156 +0,0 @@
|
||||
import os
|
||||
import streamlit as st
|
||||
import google.genai as genai
|
||||
from google.genai import types
|
||||
from google.genai.types import Tool, GenerateContentConfig, GoogleSearch
|
||||
|
||||
# Set page config
|
||||
st.set_page_config(
|
||||
page_title="Gemini Grounding Search",
|
||||
page_icon="🔍",
|
||||
layout="wide"
|
||||
)
|
||||
|
||||
# Custom CSS for styling
|
||||
st.markdown("""
|
||||
<style>
|
||||
.container {
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
font-family: Google Sans, Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 8px 12px;
|
||||
background-color: #fafafa;
|
||||
box-shadow: 0 0 0 1px #0000000f;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.chip {
|
||||
display: inline-block;
|
||||
border: solid 1px;
|
||||
border-radius: 16px;
|
||||
min-width: 14px;
|
||||
padding: 5px 16px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
margin: 0 8px;
|
||||
background-color: #ffffff;
|
||||
border-color: #d2d2d2;
|
||||
color: #5e5e5e;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chip:hover {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
.carousel {
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
white-space: nowrap;
|
||||
margin-right: -12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.headline {
|
||||
display: flex;
|
||||
margin-right: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
.gradient-container {
|
||||
position: relative;
|
||||
}
|
||||
.gradient {
|
||||
position: absolute;
|
||||
transform: translate(3px, -9px);
|
||||
height: 36px;
|
||||
width: 9px;
|
||||
background: linear-gradient(90deg, #fafafa 15%, #fafafa00 100%);
|
||||
}
|
||||
.result-text {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: #202124;
|
||||
margin: 20px 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.container {
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: 0 0 0 1px #ffffff26;
|
||||
}
|
||||
.headline-label {
|
||||
color: #fff;
|
||||
}
|
||||
.chip {
|
||||
background-color: #2c2c2c;
|
||||
border-color: #3c4043;
|
||||
color: #fff;
|
||||
}
|
||||
.chip:hover {
|
||||
background-color: #353536;
|
||||
}
|
||||
.gradient {
|
||||
background: linear-gradient(90deg, #1f1f1f 15%, #1f1f1f00 100%);
|
||||
}
|
||||
.result-text {
|
||||
color: #e8eaed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Title
|
||||
st.title("Gemini Grounding Search")
|
||||
|
||||
# Initialize Gemini client
|
||||
if 'GEMINI_API_KEY' not in os.environ:
|
||||
api_key = st.text_input("Enter your Gemini API Key:", type="password")
|
||||
if api_key:
|
||||
os.environ['GEMINI_API_KEY'] = api_key
|
||||
|
||||
# Search input
|
||||
search_query = st.text_input("Enter your search query:", "When is the next total solar eclipse in the United States?")
|
||||
|
||||
if st.button("Search"):
|
||||
if 'GEMINI_API_KEY' not in os.environ:
|
||||
st.error("Please enter your Gemini API Key first!")
|
||||
else:
|
||||
try:
|
||||
client = genai.Client(api_key=os.environ['GEMINI_API_KEY'])
|
||||
model_id = "gemini-2.0-flash"
|
||||
|
||||
google_search_tool = Tool(
|
||||
google_search = GoogleSearch()
|
||||
)
|
||||
|
||||
with st.spinner("Searching..."):
|
||||
response = client.models.generate_content(
|
||||
model=model_id,
|
||||
contents=search_query,
|
||||
config=GenerateContentConfig(
|
||||
tools=[google_search_tool],
|
||||
response_modalities=["TEXT"],
|
||||
)
|
||||
)
|
||||
|
||||
# Display search results header
|
||||
st.header("Search Results")
|
||||
|
||||
# Display the response text
|
||||
if response.candidates[0].content.parts:
|
||||
st.markdown('<div class="result-text">' +
|
||||
response.candidates[0].content.parts[0].text.replace('\n', '<br>') +
|
||||
'</div>',
|
||||
unsafe_allow_html=True)
|
||||
|
||||
# Display the grounding metadata
|
||||
if hasattr(response.candidates[0], 'grounding_metadata') and \
|
||||
hasattr(response.candidates[0].grounding_metadata, 'search_entry_point') and \
|
||||
hasattr(response.candidates[0].grounding_metadata.search_entry_point, 'rendered_content'):
|
||||
|
||||
st.header("Related Searches")
|
||||
rendered_content = response.candidates[0].grounding_metadata.search_entry_point.rendered_content
|
||||
st.markdown(rendered_content, unsafe_allow_html=True)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"An error occurred: {str(e)}")
|
||||
@@ -1,108 +0,0 @@
|
||||
import re #additional import for regex
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key=os.getenv('OPENAI-API-KEY')
|
||||
)
|
||||
|
||||
# Target URL can be a website url or it can google search
|
||||
query = "kedarkanta trek"
|
||||
target_url = f"https://www.google.com/search?q={query}&gl=us"
|
||||
response = requests.get(target_url)
|
||||
print
|
||||
html_text = response.text
|
||||
|
||||
# Remove unnecessary part to prevent HUGE TOKEN cost!
|
||||
# Remove everything between <head> and </head>
|
||||
html_text = re.sub(r'<head.*?>.*?</head>', '', html_text, flags=re.DOTALL)
|
||||
# Remove all occurrences of content between <script> and </script>
|
||||
html_text = re.sub(r'<script.*?>.*?</script>', '', html_text, flags=re.DOTALL)
|
||||
# Remove all occurrences of content between <style> and </style>
|
||||
html_text = re.sub(r'<style.*?>.*?</style>', '', html_text, flags=re.DOTALL)
|
||||
|
||||
completion = client.chat.completions.create(
|
||||
model="gpt-4-1106-preview",
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a master at scraping Google results data. Scrape two things: 1st. Scrape top 10 organic results data and 2nd. Scrape people_also_ask section from Google search result page."},
|
||||
{"role": "user", "content": html_text}
|
||||
],
|
||||
tools=[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "parse_organic_results",
|
||||
"description": "Parse organic results from Google SERP raw HTML data nicely",
|
||||
"parameters": {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'data': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'title': {'type': 'string'},
|
||||
'original_url': {'type': 'string'},
|
||||
'snippet': {'type': 'string'},
|
||||
'position': {'type': 'integer'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "parse_people_also_ask_section",
|
||||
"description": "Parse `people also ask` section from Google SERP raw HTML",
|
||||
"parameters": {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'data': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'question': {'type': 'string'},
|
||||
'original_url': {'type': 'string'},
|
||||
'answer': {'type': 'string'},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
tool_choice="auto"
|
||||
)
|
||||
|
||||
|
||||
# Organic_results
|
||||
argument_str = completion.choices[0].message.tool_calls[0].function.arguments
|
||||
argument_dict = json.loads(argument_str)
|
||||
organic_results = argument_dict['data']
|
||||
|
||||
print('Organic results:')
|
||||
for result in organic_results:
|
||||
print(f"Blog Title: {result['title']}")
|
||||
print(f"Blog URL: {result['original_url']}")
|
||||
print(f"Blog Snippet: {result['snippet']}")
|
||||
print(f"Blog Position: {result['position']}")
|
||||
print('---')
|
||||
|
||||
# People also ask
|
||||
argument_str = completion.choices[0].message.tool_calls[1].function.arguments
|
||||
argument_dict = json.loads(argument_str)
|
||||
people_also_ask = argument_dict['data']
|
||||
|
||||
print('People also ask:')
|
||||
for result in people_also_ask:
|
||||
print(f"People_Also_Ask: Question: {result['question']}")
|
||||
print(f"People_Also_Ask: URL: {result['original_url']}")
|
||||
print("People_Also_Ask: Answer: {result['answer']}")
|
||||
print('---')
|
||||
@@ -1,30 +0,0 @@
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def summarize_competitor_content(research_content):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
|
||||
prompt = f"""You are a helpful assistant writing a research report about a company. I will provide you with company details.
|
||||
Summarize the given company details into multiple paragraphs.
|
||||
Be extremely concise, professional, and factual as possible.
|
||||
The first paragraph should be an introduction and summary of the company.
|
||||
The second paragraph should include pros and cons of the company.
|
||||
The third paragraph should be on their pricing model.
|
||||
Include a conclusion, summarizing your research about the given company details.
|
||||
Company details: '{research_content}'"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from LLM: {err}")
|
||||
raise err
|
||||
@@ -1,23 +0,0 @@
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def summarize_web_content(page_content, gpt_providers="openai"):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
|
||||
prompt = f"""You are a helpful assistant that briefly summarizes the content of a webpage.
|
||||
Summarize the given web page content below.
|
||||
Web page content: '{page_content}'"""
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"summarize_web_content: Failed to get response from LLM: {err}")
|
||||
raise err
|
||||
@@ -1,129 +0,0 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
from clint.textui import progress
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path('../../.env'))
|
||||
|
||||
|
||||
def search_ydc_index(search_query, num_web_results=10, country="IN"):
|
||||
"""
|
||||
Search YDC Index API and retrieve results.
|
||||
|
||||
Args:
|
||||
search_query (str): The search query.
|
||||
num_web_results (int): Number of web results to retrieve.
|
||||
country (str): Country code.
|
||||
api_key (str): YDC Index API key.
|
||||
|
||||
Returns:
|
||||
dict: The response from the YDC Index API in JSON format.
|
||||
"""
|
||||
api_key = os.environ["YOU_API_KEY"]
|
||||
try:
|
||||
url = "https://api.ydc-index.io/search"
|
||||
|
||||
querystring = {
|
||||
"query": search_query,
|
||||
}
|
||||
|
||||
headers = {"X-API-Key": api_key}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring, stream=True)
|
||||
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
|
||||
|
||||
result_json = response.json()
|
||||
return result_json
|
||||
|
||||
except requests.exceptions.RequestException as req_exc:
|
||||
logger.error(f"Request to YDC Index API failed: {req_exc}")
|
||||
return {"error": str(req_exc)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def get_rag_results(search_query, num_web_results=10, country="IN"):
|
||||
"""
|
||||
Retrieve RAG (Relevance, Authority, and Goodness) results from YDC Index API.
|
||||
|
||||
Args:
|
||||
search_query (str): The search query.
|
||||
num_web_results (int): Number of web results to retrieve.
|
||||
country (str): Country code
|
||||
|
||||
Returns:
|
||||
dict: The response from the YDC Index API in JSON format.
|
||||
"""
|
||||
api_key = os.environ["YOU_API_KEY"]
|
||||
try:
|
||||
url = "https://api.ydc-index.io/rag"
|
||||
|
||||
querystring = {
|
||||
"query": search_query,
|
||||
"num_web_results": str(num_web_results),
|
||||
"country": country
|
||||
}
|
||||
|
||||
headers = {"X-API-Key": api_key}
|
||||
|
||||
with progress.Bar(expected_size=num_web_results, label="Fetching RAG Results") as bar:
|
||||
response = requests.get(url, headers=headers, params=querystring, stream=True)
|
||||
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
|
||||
|
||||
result_json = response.json()
|
||||
bar.show(result_json.get("web_results", [])) # Update progress bar with the number of web results
|
||||
|
||||
return result_json
|
||||
|
||||
except requests.exceptions.RequestException as req_exc:
|
||||
logger.error(f"Request to YDC Index API failed: {req_exc}")
|
||||
return {"error": str(req_exc)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def get_news_results(query, spellcheck=True):
|
||||
"""
|
||||
Retrieve news results from YDC Index API.
|
||||
|
||||
Args:
|
||||
query (str): The search query.
|
||||
spellcheck (bool): Whether to enable spellcheck.
|
||||
api_key (str): YDC Index API key.
|
||||
|
||||
Returns:
|
||||
dict: The response from the YDC Index API in JSON format.
|
||||
"""
|
||||
api_key = os.environ["YOU_API_KEY"]
|
||||
try:
|
||||
url = "https://api.ydc-index.io/news"
|
||||
|
||||
querystring = {
|
||||
"q": query,
|
||||
"spellcheck": str(spellcheck).lower()
|
||||
}
|
||||
|
||||
headers = {"X-API-Key": api_key}
|
||||
|
||||
with progress.Bar(expected_size=1, label="Fetching News Results") as bar:
|
||||
response = requests.get(url, headers=headers, params=querystring, stream=True)
|
||||
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
|
||||
|
||||
result_json = response.json()
|
||||
bar.show() # Update progress bar
|
||||
|
||||
return result_json
|
||||
|
||||
except requests.exceptions.RequestException as req_exc:
|
||||
logger.error(f"Request to YDC Index API failed: {req_exc}")
|
||||
return {"error": str(req_exc)}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred: {e}")
|
||||
return {"error": str(e)}
|
||||
@@ -1,705 +0,0 @@
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
from typing import List, Dict, Any, Callable
|
||||
|
||||
# Import existing tools
|
||||
from lib.ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.image_alt_text_generator import alt_text_gen
|
||||
from lib.ai_seo_tools.opengraph_generator import og_tag_generator
|
||||
from lib.ai_seo_tools.optimize_images_for_upload import main_img_optimizer
|
||||
from lib.ai_seo_tools.google_pagespeed_insights import google_pagespeed_insights
|
||||
from lib.ai_seo_tools.on_page_seo_analyzer import analyze_onpage_seo
|
||||
from lib.ai_seo_tools.weburl_seo_checker import url_seo_checker
|
||||
from lib.ai_marketing_tools.ai_backlinker.backlinking_ui_streamlit import backlinking_ui
|
||||
from lib.ai_seo_tools.content_gap_analysis.ui import ContentGapAnalysisUI
|
||||
from lib.ai_seo_tools.content_gap_analysis.enhanced_ui import render_enhanced_content_gap_analysis
|
||||
from lib.ai_seo_tools.content_calendar.ui.dashboard import ContentCalendarDashboard
|
||||
from lib.ai_seo_tools.technical_seo_crawler import render_technical_seo_crawler
|
||||
|
||||
# Import additional tools
|
||||
from lib.ai_seo_tools.twitter_tags_generator import display_app as twitter_tags_app
|
||||
from lib.ai_seo_tools.sitemap_analysis import main as sitemap_analyzer
|
||||
from lib.ai_seo_tools.textstaty import analyze_text as readability_analyzer
|
||||
from lib.ai_seo_tools.wordcloud import generate_wordcloud
|
||||
|
||||
# Import new enterprise tools
|
||||
from ..ai_seo_tools.google_search_console_integration import render_gsc_integration
|
||||
from ..ai_seo_tools.ai_content_strategy import render_ai_content_strategy
|
||||
from ..ai_seo_tools.enterprise_seo_suite import render_enterprise_seo_suite
|
||||
|
||||
from lib.alwrity_ui.dashboard_styles import apply_dashboard_style, render_dashboard_header, render_category_header, render_card
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL CONFIGURATION FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_enterprise_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for enterprise tools."""
|
||||
return [
|
||||
{
|
||||
'name': '🎯 Enterprise SEO Suite',
|
||||
'description': 'Unified command center for comprehensive SEO management with AI-powered workflows',
|
||||
'function': render_enterprise_seo_suite,
|
||||
'features': ['Complete SEO audit workflows', 'AI-powered recommendations', 'Strategic planning', 'Performance tracking']
|
||||
},
|
||||
{
|
||||
'name': '📊 Google Search Console Intelligence',
|
||||
'description': 'AI-powered insights from Google Search Console data with content recommendations',
|
||||
'function': render_gsc_integration,
|
||||
'features': ['GSC data analysis', 'Content opportunities', 'Performance insights', 'Strategic recommendations']
|
||||
},
|
||||
{
|
||||
'name': '🧠 AI Content Strategy Generator',
|
||||
'description': 'Generate comprehensive content strategies using AI market intelligence',
|
||||
'function': render_ai_content_strategy,
|
||||
'features': ['Content pillar development', 'Topic cluster strategy', 'Content calendar planning', 'Distribution strategy']
|
||||
}
|
||||
]
|
||||
|
||||
def get_analytics_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for analytics tools."""
|
||||
return [
|
||||
{
|
||||
'name': '📊 Google Search Console Intelligence',
|
||||
'description': 'Deep analysis of GSC data with AI-powered content recommendations',
|
||||
'function': render_gsc_integration,
|
||||
'category': 'Search Analytics'
|
||||
},
|
||||
{
|
||||
'name': '🔍 Enhanced Content Gap Analysis',
|
||||
'description': 'Advanced competitor content analysis with AI insights',
|
||||
'function': lambda: render_enhanced_content_gap_analysis(),
|
||||
'category': 'Competitive Intelligence'
|
||||
},
|
||||
{
|
||||
'name': '📈 SEO Performance Tracker',
|
||||
'description': 'Track and analyze SEO performance with trend analysis',
|
||||
'function': lambda: st.info("SEO Performance Tracker - Coming soon with advanced metrics"),
|
||||
'category': 'Performance Analytics'
|
||||
}
|
||||
]
|
||||
|
||||
def get_technical_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for technical SEO tools."""
|
||||
return [
|
||||
{
|
||||
'name': '🔍 Technical SEO Crawler',
|
||||
'description': 'Comprehensive site-wide technical SEO analysis',
|
||||
'function': lambda: render_technical_seo_crawler(),
|
||||
'priority': 'High'
|
||||
},
|
||||
{
|
||||
'name': '📱 Mobile SEO Analyzer',
|
||||
'description': 'Mobile-specific SEO analysis and optimization',
|
||||
'function': lambda: st.info("Mobile SEO Analyzer - Advanced mobile optimization coming soon"),
|
||||
'priority': 'Medium'
|
||||
},
|
||||
{
|
||||
'name': '⚡ Core Web Vitals Optimizer',
|
||||
'description': 'Analyze and optimize Core Web Vitals performance',
|
||||
'function': lambda: st.info("Core Web Vitals Optimizer - Performance optimization coming soon"),
|
||||
'priority': 'High'
|
||||
},
|
||||
{
|
||||
'name': '🗺️ XML Sitemap Generator',
|
||||
'description': 'Generate and optimize XML sitemaps',
|
||||
'function': lambda: st.info("XML Sitemap Generator - Coming soon"),
|
||||
'priority': 'Medium'
|
||||
}
|
||||
]
|
||||
|
||||
def get_content_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for content and strategy tools."""
|
||||
return [
|
||||
{
|
||||
'name': '🧠 AI Content Strategy Generator',
|
||||
'description': 'Comprehensive content strategy with AI market intelligence',
|
||||
'function': render_ai_content_strategy,
|
||||
'type': 'Enterprise'
|
||||
},
|
||||
{
|
||||
'name': '📅 Content Calendar Planner',
|
||||
'description': 'AI-powered content calendar with SEO optimization',
|
||||
'function': lambda: render_content_calendar(),
|
||||
'type': 'Professional'
|
||||
},
|
||||
{
|
||||
'name': '🎯 Topic Cluster Generator',
|
||||
'description': 'Generate SEO topic clusters for content dominance',
|
||||
'function': lambda: st.info("Topic Cluster Generator - Advanced clustering coming soon"),
|
||||
'type': 'Professional'
|
||||
},
|
||||
{
|
||||
'name': '📊 Content Performance Analyzer',
|
||||
'description': 'Analyze content performance and optimization opportunities',
|
||||
'function': lambda: st.info("Content Performance Analyzer - Coming soon"),
|
||||
'type': 'Standard'
|
||||
}
|
||||
]
|
||||
|
||||
def get_basic_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for basic SEO tools."""
|
||||
return [
|
||||
{
|
||||
'name': '📝 Meta Description Generator',
|
||||
'description': 'Generate SEO-optimized meta descriptions',
|
||||
'function': lambda: metadesc_generator_main(),
|
||||
'category': 'Metadata'
|
||||
},
|
||||
{
|
||||
'name': '🎯 Content Title Generator',
|
||||
'description': 'Create compelling, SEO-friendly titles',
|
||||
'function': lambda: ai_title_generator(),
|
||||
'category': 'Content'
|
||||
},
|
||||
{
|
||||
'name': '🔗 OpenGraph Generator',
|
||||
'description': 'Generate social media OpenGraph tags',
|
||||
'function': lambda: og_tag_generator(),
|
||||
'category': 'Social'
|
||||
},
|
||||
{
|
||||
'name': '🖼️ Image Alt Text Generator',
|
||||
'description': 'Generate SEO-friendly image alt text',
|
||||
'function': lambda: alt_text_gen(),
|
||||
'category': 'Images'
|
||||
},
|
||||
{
|
||||
'name': '📋 Schema Markup Generator',
|
||||
'description': 'Generate structured data markup',
|
||||
'function': lambda: ai_structured_data(),
|
||||
'category': 'Technical'
|
||||
},
|
||||
{
|
||||
'name': '🔍 On-Page SEO Analyzer',
|
||||
'description': 'Comprehensive on-page SEO analysis',
|
||||
'function': lambda: analyze_onpage_seo(),
|
||||
'category': 'Analysis'
|
||||
},
|
||||
{
|
||||
'name': '🌐 URL SEO Checker',
|
||||
'description': 'Quick SEO check for any URL',
|
||||
'function': lambda: url_seo_checker(),
|
||||
'category': 'Analysis'
|
||||
}
|
||||
]
|
||||
|
||||
def get_tool_functions_mapping() -> Dict[str, Callable]:
|
||||
"""Get mapping of tool names to their functions for URL routing."""
|
||||
return {
|
||||
# Core content tools
|
||||
"structured_data": ai_structured_data,
|
||||
"blog_title": ai_title_generator,
|
||||
"meta_description": metadesc_generator_main,
|
||||
"alt_text": alt_text_gen,
|
||||
"opengraph": og_tag_generator,
|
||||
"image_optimizer": main_img_optimizer,
|
||||
|
||||
# Technical analysis tools
|
||||
"technical_seo_crawler": render_technical_seo_crawler,
|
||||
"pagespeed": google_pagespeed_insights,
|
||||
"onpage_seo": analyze_onpage_seo,
|
||||
"url_checker": url_seo_checker,
|
||||
"sitemap_analysis": sitemap_analyzer,
|
||||
|
||||
# Social media tools
|
||||
"twitter_tags": render_twitter_tags,
|
||||
|
||||
# Content analysis tools
|
||||
"readability_analyzer": render_readability_analyzer,
|
||||
"wordcloud_generator": render_wordcloud_generator,
|
||||
|
||||
# Advanced tools
|
||||
"backlinking": backlinking_ui,
|
||||
"content_gap_analysis": render_content_gap_analysis,
|
||||
"enhanced_content_gap_analysis": render_enhanced_content_gap_analysis_ui,
|
||||
"content_calendar": render_content_calendar,
|
||||
|
||||
# Tool combinations for workflow efficiency
|
||||
"content_optimization": lambda: run_tool_combination([
|
||||
ai_title_generator,
|
||||
metadesc_generator_main,
|
||||
ai_structured_data
|
||||
], "Content Optimization Suite"),
|
||||
"technical_audit": lambda: run_tool_combination([
|
||||
google_pagespeed_insights,
|
||||
analyze_onpage_seo,
|
||||
url_seo_checker
|
||||
], "Technical SEO Audit"),
|
||||
"image_optimization": lambda: run_tool_combination([
|
||||
alt_text_gen,
|
||||
main_img_optimizer
|
||||
], "Image Optimization Suite"),
|
||||
"social_optimization": lambda: run_tool_combination([
|
||||
og_tag_generator,
|
||||
render_twitter_tags
|
||||
], "Social Media Optimization")
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# INDIVIDUAL TOOL RENDERING FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def render_content_gap_analysis():
|
||||
"""Render the content gap analysis workflow interface."""
|
||||
ui = ContentGapAnalysisUI()
|
||||
ui.run()
|
||||
|
||||
def render_enhanced_content_gap_analysis_ui():
|
||||
"""Render the enhanced content gap analysis with advertools integration."""
|
||||
render_enhanced_content_gap_analysis()
|
||||
|
||||
def render_content_calendar():
|
||||
"""Render the content calendar dashboard with proper error handling."""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('content_calendar.log', mode='a')
|
||||
]
|
||||
)
|
||||
calendar_logger = logging.getLogger('content_calendar')
|
||||
|
||||
try:
|
||||
calendar_logger.info("Initializing Content Calendar Dashboard")
|
||||
dashboard = ContentCalendarDashboard()
|
||||
calendar_logger.info("Rendering Content Calendar Dashboard")
|
||||
dashboard.render()
|
||||
calendar_logger.info("Content Calendar Dashboard rendered successfully")
|
||||
except Exception as e:
|
||||
calendar_logger.error(f"Error rendering content calendar: {str(e)}", exc_info=True)
|
||||
st.error(f"An error occurred while loading the content calendar: {str(e)}")
|
||||
|
||||
def render_twitter_tags():
|
||||
"""Render the Twitter tags generator."""
|
||||
twitter_tags_app()
|
||||
|
||||
def render_readability_analyzer():
|
||||
"""Render the text readability analyzer."""
|
||||
st.title("📖 Text Readability Analyzer")
|
||||
st.write("Making Your Content Easy to Read")
|
||||
|
||||
text_input = st.text_area("Paste your text here:", height=200)
|
||||
|
||||
if st.button("Analyze Readability"):
|
||||
if text_input.strip():
|
||||
_display_readability_metrics(text_input)
|
||||
_display_readability_recommendations()
|
||||
else:
|
||||
st.error("Please enter text to analyze.")
|
||||
|
||||
def _display_readability_metrics(text: str):
|
||||
"""Display readability metrics for the given text."""
|
||||
from textstat import textstat
|
||||
|
||||
metrics = {
|
||||
"Flesch Reading Ease": textstat.flesch_reading_ease(text),
|
||||
"Flesch-Kincaid Grade Level": textstat.flesch_kincaid_grade(text),
|
||||
"Gunning Fog Index": textstat.gunning_fog(text),
|
||||
"SMOG Index": textstat.smog_index(text),
|
||||
"Automated Readability Index": textstat.automated_readability_index(text),
|
||||
"Coleman-Liau Index": textstat.coleman_liau_index(text),
|
||||
"Linsear Write Formula": textstat.linsear_write_formula(text),
|
||||
"Dale-Chall Readability Score": textstat.dale_chall_readability_score(text),
|
||||
"Readability Consensus": textstat.readability_consensus(text)
|
||||
}
|
||||
|
||||
st.subheader("Text Analysis Results")
|
||||
for metric, value in metrics.items():
|
||||
st.metric(metric, f"{value:.2f}")
|
||||
|
||||
def _display_readability_recommendations():
|
||||
"""Display readability recommendations."""
|
||||
st.subheader("Key Takeaways:")
|
||||
st.markdown("""
|
||||
* **Don't Be Afraid to Simplify!** Often, simpler language makes content more impactful and easier to digest.
|
||||
* **Aim for a Reading Level Appropriate for Your Audience:** Consider the education level, background, and familiarity of your readers.
|
||||
* **Use Short Sentences:** This makes your content more scannable and easier to read.
|
||||
* **Write for Everyone:** Accessibility should always be a priority. When in doubt, aim for clear, concise language!
|
||||
""")
|
||||
|
||||
def render_wordcloud_generator():
|
||||
"""Render the word cloud generator."""
|
||||
st.title("☁️ Word Cloud Generator")
|
||||
st.write("Visualize the most important words in your content")
|
||||
|
||||
text_input = st.text_area("Enter your text:", height=200)
|
||||
|
||||
if st.button("Generate Word Cloud"):
|
||||
if text_input.strip():
|
||||
_generate_and_display_wordcloud(text_input)
|
||||
_display_text_statistics(text_input)
|
||||
else:
|
||||
st.error("Please enter text to generate a word cloud.")
|
||||
|
||||
def _generate_and_display_wordcloud(text: str):
|
||||
"""Generate and display word cloud for the given text."""
|
||||
from wordcloud import WordCloud
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Create and generate a word cloud image
|
||||
wordcloud = WordCloud(width=800, height=400, background_color='white').generate(text)
|
||||
|
||||
# Display the word cloud
|
||||
st.subheader("Word Cloud Visualization")
|
||||
fig, ax = plt.subplots(figsize=(10, 5))
|
||||
ax.imshow(wordcloud, interpolation='bilinear')
|
||||
ax.axis('off')
|
||||
st.pyplot(fig)
|
||||
|
||||
def _display_text_statistics(text: str):
|
||||
"""Display basic text statistics."""
|
||||
st.subheader("Text Statistics")
|
||||
words = text.split()
|
||||
unique_words = set(words)
|
||||
st.metric("Total Words", len(words))
|
||||
st.metric("Unique Words", len(unique_words))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TAB RENDERING FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def render_enterprise_tab():
|
||||
"""Render the Enterprise Suite tab."""
|
||||
st.header("🏢 Enterprise SEO Command Center")
|
||||
st.markdown("**Unified SEO management for enterprise-level optimization**")
|
||||
|
||||
enterprise_tools = get_enterprise_tools_config()
|
||||
|
||||
# Display enterprise tools
|
||||
for tool in enterprise_tools:
|
||||
_render_enterprise_tool_card(tool)
|
||||
|
||||
# Render selected enterprise tool
|
||||
_render_selected_enterprise_tool(enterprise_tools)
|
||||
|
||||
def _render_enterprise_tool_card(tool: Dict[str, Any]):
|
||||
"""Render an individual enterprise tool card."""
|
||||
with st.expander(f"{tool['name']} - {tool['description']}", expanded=False):
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown("**Key Features:**")
|
||||
for feature in tool['features']:
|
||||
st.write(f"• {feature}")
|
||||
|
||||
with col2:
|
||||
if st.button(f"Launch {tool['name'].split()[1]}", key=f"enterprise_{tool['name']}", use_container_width=True):
|
||||
st.session_state.selected_enterprise_tool = tool['name']
|
||||
tool['function']()
|
||||
|
||||
def _render_selected_enterprise_tool(enterprise_tools: List[Dict[str, Any]]):
|
||||
"""Render the selected enterprise tool if any."""
|
||||
if 'selected_enterprise_tool' in st.session_state:
|
||||
selected_tool = next((tool for tool in enterprise_tools if tool['name'] == st.session_state.selected_enterprise_tool), None)
|
||||
if selected_tool:
|
||||
st.markdown("---")
|
||||
selected_tool['function']()
|
||||
|
||||
def render_analytics_tab():
|
||||
"""Render the Analytics & Intelligence tab."""
|
||||
st.header("📊 Analytics & Intelligence")
|
||||
st.markdown("**Advanced analytics and competitive intelligence tools**")
|
||||
|
||||
analytics_tools = get_analytics_tools_config()
|
||||
|
||||
# Group tools by category
|
||||
categories = _group_tools_by_category(analytics_tools)
|
||||
|
||||
for category, tools in categories.items():
|
||||
st.subheader(f"📊 {category}")
|
||||
|
||||
for tool in tools:
|
||||
_render_analytics_tool_row(tool)
|
||||
|
||||
def _group_tools_by_category(tools: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Group tools by their category."""
|
||||
categories = {}
|
||||
for tool in tools:
|
||||
category = tool['category']
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(tool)
|
||||
return categories
|
||||
|
||||
def _render_analytics_tool_row(tool: Dict[str, Any]):
|
||||
"""Render an analytics tool row."""
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}**")
|
||||
st.write(tool['description'])
|
||||
|
||||
with col2:
|
||||
if st.button("Launch", key=f"analytics_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
|
||||
def render_technical_tab():
|
||||
"""Render the Technical SEO tab."""
|
||||
st.header("🔧 Technical SEO")
|
||||
st.markdown("**Advanced technical SEO analysis and optimization tools**")
|
||||
|
||||
technical_tools = get_technical_tools_config()
|
||||
|
||||
# Display technical tools with priority indicators
|
||||
for tool in technical_tools:
|
||||
_render_technical_tool_row(tool)
|
||||
|
||||
def _render_technical_tool_row(tool: Dict[str, Any]):
|
||||
"""Render a technical tool row with priority indicator."""
|
||||
priority_color = "🔴" if tool['priority'] == 'High' else "🟡"
|
||||
|
||||
col1, col2, col3 = st.columns([2, 1, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}** {priority_color}")
|
||||
st.write(tool['description'])
|
||||
|
||||
with col2:
|
||||
st.write(f"**Priority:** {tool['priority']}")
|
||||
|
||||
with col3:
|
||||
if st.button("Launch", key=f"technical_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
|
||||
def render_content_tab():
|
||||
"""Render the Content & Strategy tab."""
|
||||
st.header("📝 Content & Strategy")
|
||||
st.markdown("**AI-powered content creation and strategy tools**")
|
||||
|
||||
content_tools = get_content_tools_config()
|
||||
|
||||
# Group by tool type
|
||||
tool_types = _group_tools_by_type(content_tools)
|
||||
|
||||
for tool_type, tools in tool_types.items():
|
||||
_render_content_tool_section(tool_type, tools)
|
||||
|
||||
def _group_tools_by_type(tools: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Group tools by their type."""
|
||||
tool_types = {}
|
||||
for tool in tools:
|
||||
tool_type = tool['type']
|
||||
if tool_type not in tool_types:
|
||||
tool_types[tool_type] = []
|
||||
tool_types[tool_type].append(tool)
|
||||
return tool_types
|
||||
|
||||
def _render_content_tool_section(tool_type: str, tools: List[Dict[str, Any]]):
|
||||
"""Render a content tool section."""
|
||||
type_color = {"Enterprise": "🏢", "Professional": "💼", "Standard": "📋"}
|
||||
st.subheader(f"{type_color.get(tool_type, '📋')} {tool_type} Tools")
|
||||
|
||||
for tool in tools:
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}**")
|
||||
st.write(tool['description'])
|
||||
|
||||
with col2:
|
||||
if st.button("Launch", key=f"content_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
|
||||
def render_basic_tools_tab():
|
||||
"""Render the Basic Tools tab."""
|
||||
st.header("🎯 Basic SEO Tools")
|
||||
st.markdown("**Essential SEO tools for quick optimization tasks**")
|
||||
|
||||
basic_tools = get_basic_tools_config()
|
||||
|
||||
# Group basic tools by category
|
||||
basic_categories = _group_tools_by_category(basic_tools)
|
||||
|
||||
# Display in columns for better layout
|
||||
_render_basic_tools_in_columns(basic_categories)
|
||||
|
||||
def _render_basic_tools_in_columns(basic_categories: Dict[str, List[Dict[str, Any]]]):
|
||||
"""Render basic tools in two columns."""
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
categories_list = list(basic_categories.items())
|
||||
mid_point = len(categories_list) // 2
|
||||
|
||||
with col1:
|
||||
for category, tools in categories_list[:mid_point]:
|
||||
_render_basic_tool_category(category, tools)
|
||||
|
||||
with col2:
|
||||
for category, tools in categories_list[mid_point:]:
|
||||
_render_basic_tool_category(category, tools)
|
||||
|
||||
def _render_basic_tool_category(category: str, tools: List[Dict[str, Any]]):
|
||||
"""Render a basic tool category."""
|
||||
st.subheader(f"📂 {category}")
|
||||
for tool in tools:
|
||||
if st.button(f"{tool['name']}", key=f"basic_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
st.caption(tool['description'])
|
||||
st.markdown("---")
|
||||
|
||||
def render_enterprise_features_footer():
|
||||
"""Render the enterprise features footer."""
|
||||
st.markdown("---")
|
||||
st.markdown("### 🚀 Enterprise SEO Features")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.info("""
|
||||
**🏢 Enterprise Suite**
|
||||
- Unified SEO workflows
|
||||
- AI-powered insights
|
||||
- Strategic planning
|
||||
- Performance tracking
|
||||
""")
|
||||
|
||||
with col2:
|
||||
st.info("""
|
||||
**📊 Advanced Analytics**
|
||||
- GSC integration
|
||||
- Competitive intelligence
|
||||
- Content gap analysis
|
||||
- Performance insights
|
||||
""")
|
||||
|
||||
with col3:
|
||||
st.info("""
|
||||
**🧠 AI Strategy**
|
||||
- Content strategy generation
|
||||
- Topic cluster planning
|
||||
- Distribution optimization
|
||||
- Market intelligence
|
||||
""")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN DASHBOARD FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def render_seo_tools_dashboard():
|
||||
"""Render comprehensive SEO tools dashboard with enterprise features."""
|
||||
st.title("🚀 Alwrity AI SEO Tools")
|
||||
st.markdown("**Enterprise-level SEO tools powered by artificial intelligence**")
|
||||
|
||||
# Create tabs for different tool categories
|
||||
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
||||
"🏢 Enterprise Suite",
|
||||
"📊 Analytics & Intelligence",
|
||||
"🔧 Technical SEO",
|
||||
"📝 Content & Strategy",
|
||||
"🎯 Basic Tools"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
render_enterprise_tab()
|
||||
|
||||
with tab2:
|
||||
render_analytics_tab()
|
||||
|
||||
with tab3:
|
||||
render_technical_tab()
|
||||
|
||||
with tab4:
|
||||
render_content_tab()
|
||||
|
||||
with tab5:
|
||||
render_basic_tools_tab()
|
||||
|
||||
# Add footer with enterprise features highlight
|
||||
render_enterprise_features_footer()
|
||||
|
||||
def ai_seo_tools():
|
||||
"""Main entry point for SEO tools dashboard with premium glassmorphic design."""
|
||||
logger.info("Starting SEO Tools Dashboard")
|
||||
|
||||
# Apply common dashboard styling
|
||||
apply_dashboard_style()
|
||||
|
||||
# Check if a specific tool is selected
|
||||
selected_tool = st.query_params.get("tool")
|
||||
|
||||
if selected_tool:
|
||||
_handle_selected_tool(selected_tool)
|
||||
else:
|
||||
# Show the dashboard if no tool is selected
|
||||
render_seo_tools_dashboard()
|
||||
|
||||
def _handle_selected_tool(selected_tool: str):
|
||||
"""Handle rendering of a specific selected tool."""
|
||||
tool_functions = get_tool_functions_mapping()
|
||||
|
||||
if selected_tool in tool_functions:
|
||||
# Clear any existing content
|
||||
st.empty()
|
||||
# Execute the selected tool's function
|
||||
tool_functions[selected_tool]()
|
||||
else:
|
||||
st.error(f"Tool '{selected_tool}' is not available or under development.")
|
||||
st.info("Please select a different tool from the dashboard.")
|
||||
render_seo_tools_dashboard()
|
||||
|
||||
def run_tool_combination(tools: List[Callable], combination_name: str):
|
||||
"""Run a combination of tools and provide cross-tool analysis."""
|
||||
st.markdown(f"# {combination_name}")
|
||||
st.markdown("Comprehensive SEO analysis workflow")
|
||||
|
||||
# Create tabs for each tool in the combination
|
||||
tab_names = _generate_tab_names(tools)
|
||||
tabs = st.tabs(tab_names)
|
||||
|
||||
# Run each tool in its own tab
|
||||
_execute_tools_in_tabs(tabs, tools)
|
||||
|
||||
# Add cross-tool analysis section
|
||||
_render_analysis_summary()
|
||||
|
||||
def _generate_tab_names(tools: List[Callable]) -> List[str]:
|
||||
"""Generate tab names for tool combination."""
|
||||
tab_names = []
|
||||
for i, tool in enumerate(tools):
|
||||
if hasattr(tool, '__name__'):
|
||||
tab_names.append(tool.__name__.replace('_', ' ').title())
|
||||
else:
|
||||
tab_names.append(f"Step {i+1}")
|
||||
return tab_names
|
||||
|
||||
def _execute_tools_in_tabs(tabs: List, tools: List[Callable]):
|
||||
"""Execute tools in their respective tabs."""
|
||||
for tab, tool in zip(tabs, tools):
|
||||
with tab:
|
||||
try:
|
||||
tool()
|
||||
except Exception as e:
|
||||
st.error(f"Error running tool: {str(e)}")
|
||||
logger.error(f"Error in tool combination: {str(e)}")
|
||||
|
||||
def _render_analysis_summary():
|
||||
"""Render the analysis summary section."""
|
||||
with st.expander("📊 Analysis Summary", expanded=True):
|
||||
st.markdown("""
|
||||
### Key Recommendations:
|
||||
1. **Content Optimization**: Ensure your titles and meta descriptions are keyword-optimized
|
||||
2. **Technical Performance**: Address any speed or technical issues identified
|
||||
3. **Structured Data**: Implement schema markup for better search visibility
|
||||
4. **Social Optimization**: Optimize social sharing tags for better engagement
|
||||
|
||||
### Next Steps:
|
||||
- Implement the recommendations from each tool
|
||||
- Monitor your rankings and traffic after changes
|
||||
- Regularly audit your content using these tools
|
||||
""")
|
||||
|
||||
# Add export functionality placeholder
|
||||
if st.button("📥 Export Analysis Report", use_container_width=True):
|
||||
st.info("Export functionality is being developed. Save your results manually for now.")
|
||||
@@ -1,167 +0,0 @@
|
||||
# Content Calendar & Topic Planning System
|
||||
|
||||
A comprehensive content planning and scheduling system that leverages existing SEO tools and AI capabilities to create optimized content calendars based on content gap analysis.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
content_calendar/
|
||||
├── README.md
|
||||
├── core/
|
||||
│ ├── __init__.py
|
||||
│ ├── calendar_manager.py # Main calendar management system
|
||||
│ ├── topic_generator.py # AI-powered topic generation
|
||||
│ └── content_predictor.py # Content performance prediction
|
||||
├── integrations/
|
||||
│ ├── __init__.py
|
||||
│ ├── seo_tools.py # Integration with existing SEO tools
|
||||
│ ├── gap_analyzer.py # Content gap analysis integration
|
||||
│ └── platform_adapters.py # Platform-specific content adaptation
|
||||
├── models/
|
||||
│ ├── __init__.py
|
||||
│ ├── calendar.py # Calendar data models
|
||||
│ ├── content.py # Content data models
|
||||
│ └── analytics.py # Analytics data models
|
||||
├── utils/
|
||||
│ ├── __init__.py
|
||||
│ ├── date_utils.py # Date and scheduling utilities
|
||||
│ ├── validation.py # Input validation
|
||||
│ └── error_handling.py # Error handling utilities
|
||||
└── tests/
|
||||
├── __init__.py
|
||||
├── test_calendar.py
|
||||
├── test_topic_generator.py
|
||||
└── test_integrations.py
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
|
||||
1. **Basic Calendar Management**
|
||||
- Implement calendar data structures
|
||||
- Create scheduling algorithms
|
||||
- Build date management utilities
|
||||
|
||||
2. **Topic Generation System**
|
||||
- Integrate with existing AI tools
|
||||
- Implement topic generation logic
|
||||
- Add SEO optimization features
|
||||
|
||||
3. **Integration Framework**
|
||||
- Connect with existing SEO tools
|
||||
- Implement content gap analysis integration
|
||||
- Create platform-specific adapters
|
||||
|
||||
### Phase 2: AI & SEO Enhancement
|
||||
|
||||
1. **AI-Powered Features**
|
||||
- Implement topic ideation
|
||||
- Add content structure generation
|
||||
- Create performance prediction models
|
||||
|
||||
2. **SEO Optimization**
|
||||
- Integrate title optimization
|
||||
- Add meta description generation
|
||||
- Implement structured data creation
|
||||
|
||||
3. **Content Performance**
|
||||
- Add performance tracking
|
||||
- Implement analytics collection
|
||||
- Create reporting system
|
||||
|
||||
### Phase 3: UI Development
|
||||
|
||||
1. **Calendar Interface**
|
||||
- Create interactive calendar view
|
||||
- Implement drag-and-drop functionality
|
||||
- Add platform-specific views
|
||||
|
||||
2. **Content Planning Panel**
|
||||
- Build topic suggestion interface
|
||||
- Create SEO metrics display
|
||||
- Implement content gap visualization
|
||||
|
||||
3. **Analytics Dashboard**
|
||||
- Design performance metrics view
|
||||
- Create engagement tracking
|
||||
- Implement progress monitoring
|
||||
|
||||
### Phase 4: Testing & Refinement
|
||||
|
||||
1. **Testing**
|
||||
- Unit testing
|
||||
- Integration testing
|
||||
- User acceptance testing
|
||||
|
||||
2. **Optimization**
|
||||
- Performance optimization
|
||||
- Code refactoring
|
||||
- Bug fixes
|
||||
|
||||
3. **Documentation**
|
||||
- API documentation
|
||||
- User guides
|
||||
- Integration guides
|
||||
|
||||
## Integration with Existing Tools
|
||||
|
||||
### SEO Tools Integration
|
||||
- `content_title_generator.py` - For optimized titles
|
||||
- `meta_desc_generator.py` - For meta descriptions
|
||||
- `seo_structured_data.py` - For structured data
|
||||
- `content_gap_analysis/` - For gap analysis
|
||||
- `webpage_content_analysis.py` - For content analysis
|
||||
|
||||
### AI Capabilities
|
||||
- Leverage existing `llm_text_gen` for:
|
||||
- Topic generation
|
||||
- Content structure
|
||||
- Performance prediction
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Content Planning**
|
||||
- AI-powered topic generation
|
||||
- SEO-optimized content scheduling
|
||||
- Platform-specific planning
|
||||
|
||||
2. **SEO Integration**
|
||||
- Automated SEO optimization
|
||||
- Performance tracking
|
||||
- Gap analysis integration
|
||||
|
||||
3. **Analytics & Reporting**
|
||||
- Content performance metrics
|
||||
- SEO impact tracking
|
||||
- Platform engagement stats
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Prerequisites**
|
||||
- Python 3.8+
|
||||
- Access to existing SEO tools
|
||||
- Required API keys
|
||||
|
||||
2. **Installation**
|
||||
```bash
|
||||
# Add installation steps here
|
||||
```
|
||||
|
||||
3. **Configuration**
|
||||
```python
|
||||
# Add configuration example here
|
||||
```
|
||||
|
||||
4. **Basic Usage**
|
||||
```python
|
||||
# Add usage example here
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Guidelines for contributing to the project.
|
||||
|
||||
## License
|
||||
|
||||
Project license information.
|
||||
@@ -1,754 +0,0 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import json
|
||||
|
||||
# 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 ContentType, ContentItem, Platform
|
||||
from lib.ai_seo_tools.content_calendar.utils.error_handling import handle_calendar_error
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AIGenerator:
|
||||
"""AI-powered content generation and enhancement."""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.ai_generator')
|
||||
self.logger.info("Initializing AIGenerator")
|
||||
self._setup_logging()
|
||||
self._load_ai_tools()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Configure logging for AI generator."""
|
||||
logger.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
def _load_ai_tools(self):
|
||||
"""Load and initialize AI tools."""
|
||||
try:
|
||||
# Initialize AI tools
|
||||
self.gap_analyzer = ContentGapAnalysis()
|
||||
self.title_generator = ai_title_generator
|
||||
self.meta_generator = metadesc_generator_main
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading AI tools: {str(e)}")
|
||||
raise
|
||||
|
||||
def generate_content(self, content_item: ContentItem, target_audience: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate base content using AI."""
|
||||
try:
|
||||
self.logger.info(f"Generating content for: {content_item.title}")
|
||||
|
||||
# Generate content based on type and platform
|
||||
content = {
|
||||
'title': content_item.title,
|
||||
'content_flow': {
|
||||
'introduction': {
|
||||
'summary': f"An engaging introduction about {content_item.title}",
|
||||
'key_points': [
|
||||
f"Key point 1 about {content_item.title}",
|
||||
f"Key point 2 about {content_item.title}",
|
||||
f"Key point 3 about {content_item.title}"
|
||||
]
|
||||
},
|
||||
'main_content': {
|
||||
'sections': [
|
||||
{
|
||||
'title': f"Section 1: Understanding {content_item.title}",
|
||||
'content': f"Detailed content about {content_item.title}",
|
||||
'subsections': []
|
||||
},
|
||||
{
|
||||
'title': f"Section 2: Best Practices for {content_item.title}",
|
||||
'content': "Best practices and recommendations",
|
||||
'subsections': []
|
||||
}
|
||||
]
|
||||
},
|
||||
'conclusion': {
|
||||
'summary': f"Concluding thoughts about {content_item.title}",
|
||||
'call_to_action': "Next steps and actions"
|
||||
}
|
||||
},
|
||||
'metadata': {
|
||||
'tone': target_audience.get('content_settings', {}).get('tone', 'professional'),
|
||||
'length': target_audience.get('content_settings', {}).get('length', 'medium'),
|
||||
'platform': content_item.platforms[0].name if content_item.platforms else 'Unknown',
|
||||
'content_type': content_item.content_type.name
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating content: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def enhance_content(self, content: ContentItem, enhancement_type: str, target_audience: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Enhance existing content using AI."""
|
||||
try:
|
||||
self.logger.info(f"Enhancing content: {content.title}")
|
||||
|
||||
# Enhance content based on type
|
||||
enhanced = {
|
||||
'content': f"Enhanced version of {content.description}",
|
||||
'changes': [
|
||||
"Improved readability",
|
||||
"Enhanced engagement elements",
|
||||
"Optimized for target audience"
|
||||
],
|
||||
'metadata': {
|
||||
'enhancement_type': enhancement_type,
|
||||
'target_audience': target_audience
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enhancing content: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def enhance_for_platform(self, content: Dict[str, Any], platform: Platform, enhancement_type: str) -> Dict[str, Any]:
|
||||
"""Enhance content specifically for a platform."""
|
||||
try:
|
||||
self.logger.info(f"Enhancing content for platform: {platform.name}")
|
||||
|
||||
# Platform-specific enhancements
|
||||
enhanced = {
|
||||
'content': content.get('content', ''),
|
||||
'changes': [
|
||||
f"Optimized for {platform.name}",
|
||||
"Platform-specific formatting",
|
||||
"Enhanced engagement elements"
|
||||
],
|
||||
'metadata': {
|
||||
'platform': platform.name,
|
||||
'enhancement_type': enhancement_type
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enhancing for platform: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def enhance_variant(self, content: Dict[str, Any], variant_type: str, optimization_goals: List[str]) -> Dict[str, Any]:
|
||||
"""Enhance a content variant for A/B testing."""
|
||||
try:
|
||||
self.logger.info(f"Enhancing variant: {variant_type}")
|
||||
|
||||
# Variant-specific enhancements
|
||||
enhanced = {
|
||||
'content': content.get('content', ''),
|
||||
'changes': [
|
||||
f"Optimized for {', '.join(optimization_goals)}",
|
||||
"Enhanced variant-specific elements",
|
||||
"Improved engagement metrics"
|
||||
],
|
||||
'metadata': {
|
||||
'variant_type': variant_type,
|
||||
'optimization_goals': optimization_goals
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enhancing variant: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def enhance_for_seo(self, content: Dict[str, Any], seo_goals: List[str]) -> Dict[str, Any]:
|
||||
"""Enhance content for SEO optimization."""
|
||||
try:
|
||||
self.logger.info("Enhancing content for SEO")
|
||||
|
||||
# SEO-specific enhancements
|
||||
enhanced = {
|
||||
'content': content.get('content', ''),
|
||||
'changes': [
|
||||
f"Optimized for {', '.join(seo_goals)}",
|
||||
"Enhanced keyword placement",
|
||||
"Improved meta information"
|
||||
],
|
||||
'metadata': {
|
||||
'seo_goals': seo_goals
|
||||
}
|
||||
}
|
||||
|
||||
return enhanced
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enhancing for SEO: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def generate_series_content(self, content_item: ContentItem, series_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate content for a series."""
|
||||
try:
|
||||
self.logger.info(f"Generating series content: {content_item.title}")
|
||||
|
||||
# Generate series-specific content
|
||||
content = {
|
||||
'title': content_item.title,
|
||||
'content_flow': {
|
||||
'introduction': {
|
||||
'summary': f"Part {series_info['part_number']} of {series_info['total_parts']} about {series_info['topic']}",
|
||||
'key_points': [
|
||||
f"Key point 1 for part {series_info['part_number']}",
|
||||
f"Key point 2 for part {series_info['part_number']}",
|
||||
f"Key point 3 for part {series_info['part_number']}"
|
||||
]
|
||||
},
|
||||
'main_content': {
|
||||
'sections': [
|
||||
{
|
||||
'title': f"Section 1: Part {series_info['part_number']} Overview",
|
||||
'content': f"Detailed content for part {series_info['part_number']}",
|
||||
'subsections': []
|
||||
},
|
||||
{
|
||||
'title': f"Section 2: Part {series_info['part_number']} Details",
|
||||
'content': "Specific details and information",
|
||||
'subsections': []
|
||||
}
|
||||
]
|
||||
},
|
||||
'conclusion': {
|
||||
'summary': f"Concluding thoughts for part {series_info['part_number']}",
|
||||
'next_part': f"Preview of part {series_info['part_number'] + 1}" if series_info['part_number'] < series_info['total_parts'] else "Series conclusion"
|
||||
}
|
||||
},
|
||||
'metadata': {
|
||||
'series_info': series_info,
|
||||
'platform': content_item.platforms[0].name if content_item.platforms else 'Unknown',
|
||||
'content_type': content_item.content_type.name
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating series content: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_headings(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate content headings using AI.
|
||||
|
||||
Args:
|
||||
title: Content title
|
||||
content_type: Type of content
|
||||
context: Content context from gap analysis
|
||||
|
||||
Returns:
|
||||
List of generated headings with metadata
|
||||
"""
|
||||
try:
|
||||
# Get content gaps and opportunities
|
||||
gaps = self.gap_analyzer.analyze_gaps(context.get('website_url', ''))
|
||||
|
||||
# Generate headings based on content type and gaps
|
||||
prompt = self._create_heading_prompt(title, content_type, gaps)
|
||||
headings = self._call_ai_model(prompt)
|
||||
|
||||
return self._format_headings(headings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating headings: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_subheadings(
|
||||
self,
|
||||
main_heading: Dict[str, Any],
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate subheadings for a main heading.
|
||||
|
||||
Args:
|
||||
main_heading: Main heading to generate subheadings for
|
||||
content_type: Type of content
|
||||
context: Content context
|
||||
|
||||
Returns:
|
||||
List of generated subheadings
|
||||
"""
|
||||
try:
|
||||
# Create prompt for subheading generation
|
||||
prompt = self._create_subheading_prompt(
|
||||
main_heading,
|
||||
content_type,
|
||||
context
|
||||
)
|
||||
|
||||
# Generate subheadings
|
||||
subheadings = self._call_ai_model(prompt)
|
||||
|
||||
return self._format_subheadings(subheadings)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating subheadings: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_key_points(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate key points for content.
|
||||
|
||||
Args:
|
||||
title: Content title
|
||||
content_type: Type of content
|
||||
context: Content context
|
||||
|
||||
Returns:
|
||||
List of key points with supporting information
|
||||
"""
|
||||
try:
|
||||
# Generate title and meta description for SEO context
|
||||
seo_title = self.title_generator(title)
|
||||
meta_desc = self.meta_generator(title)
|
||||
|
||||
# Create prompt for key points
|
||||
prompt = self._create_key_points_prompt(
|
||||
title,
|
||||
content_type,
|
||||
{'title': seo_title, 'meta_description': meta_desc},
|
||||
context
|
||||
)
|
||||
|
||||
# Generate key points
|
||||
points = self._call_ai_model(prompt)
|
||||
|
||||
return self._format_key_points(points)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating key points: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_content_flow(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
outline: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content flow and structure.
|
||||
|
||||
Args:
|
||||
title: Content title
|
||||
content_type: Type of content
|
||||
outline: Content outline with headings and key points
|
||||
|
||||
Returns:
|
||||
Dictionary containing content flow and structure
|
||||
"""
|
||||
try:
|
||||
# Create prompt for content flow
|
||||
prompt = self._create_flow_prompt(title, content_type, outline)
|
||||
|
||||
# Generate content flow
|
||||
flow = self._call_ai_model(prompt)
|
||||
|
||||
return self._format_content_flow(flow)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content flow: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _create_heading_prompt(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
gaps: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create prompt for heading generation."""
|
||||
return f"""
|
||||
Generate main headings for a {content_type.value} titled "{title}".
|
||||
Consider the following content gaps and opportunities:
|
||||
{json.dumps(gaps, indent=2)}
|
||||
|
||||
For each heading, provide:
|
||||
1. Title
|
||||
2. Level (1 for main headings)
|
||||
3. Key keywords to include
|
||||
4. Brief summary of what this section should cover
|
||||
|
||||
Format the response as a JSON array of heading objects.
|
||||
"""
|
||||
|
||||
def _create_subheading_prompt(
|
||||
self,
|
||||
main_heading: Dict[str, Any],
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create prompt for subheading generation."""
|
||||
return f"""
|
||||
Generate subheadings for the main heading "{main_heading['title']}"
|
||||
in a {content_type.value}.
|
||||
|
||||
Main heading details:
|
||||
{json.dumps(main_heading, indent=2)}
|
||||
|
||||
For each subheading, provide:
|
||||
1. Title
|
||||
2. Level (2 for subheadings)
|
||||
3. Key keywords to include
|
||||
4. Brief summary of what this subsection should cover
|
||||
|
||||
Format the response as a JSON array of subheading objects.
|
||||
"""
|
||||
|
||||
def _create_key_points_prompt(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
seo_data: Dict[str, Any],
|
||||
context: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create prompt for key points generation."""
|
||||
return f"""
|
||||
Generate key points for a {content_type.value} titled "{title}".
|
||||
|
||||
SEO Requirements:
|
||||
{json.dumps(seo_data, indent=2)}
|
||||
|
||||
For each key point, provide:
|
||||
1. Main point
|
||||
2. Importance level (high/medium/low)
|
||||
3. Supporting evidence or examples
|
||||
4. Related keywords to include
|
||||
|
||||
Format the response as a JSON array of key point objects.
|
||||
"""
|
||||
|
||||
def _create_flow_prompt(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
outline: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create prompt for content flow generation."""
|
||||
return f"""
|
||||
Generate content flow and structure for a {content_type.value} titled "{title}".
|
||||
|
||||
Content Outline:
|
||||
{json.dumps(outline, indent=2)}
|
||||
|
||||
Provide:
|
||||
1. Introduction structure
|
||||
2. Main sections flow
|
||||
3. Conclusion approach
|
||||
4. Transition points between sections
|
||||
5. Content pacing recommendations
|
||||
|
||||
Format the response as a JSON object with these sections.
|
||||
"""
|
||||
|
||||
def _call_ai_model(self, prompt: str) -> Any:
|
||||
"""
|
||||
Call the AI model with the given prompt.
|
||||
|
||||
Args:
|
||||
prompt: The prompt to send to the AI model
|
||||
|
||||
Returns:
|
||||
The AI model's response, parsed as JSON
|
||||
"""
|
||||
try:
|
||||
# Call the AI model
|
||||
response = llm_text_gen(
|
||||
prompt=prompt,
|
||||
max_tokens=1000,
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
frequency_penalty=0.5,
|
||||
presence_penalty=0.5
|
||||
)
|
||||
|
||||
# Parse the response as JSON
|
||||
try:
|
||||
return json.loads(response)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error parsing AI response as JSON: {str(e)}")
|
||||
logger.error(f"Raw response: {response}")
|
||||
return {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling AI model: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _format_headings(self, headings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Format and validate generated headings."""
|
||||
formatted = []
|
||||
for heading in headings:
|
||||
formatted.append({
|
||||
'title': heading.get('title', ''),
|
||||
'level': heading.get('level', 1),
|
||||
'keywords': heading.get('keywords', []),
|
||||
'summary': heading.get('summary', '')
|
||||
})
|
||||
return formatted
|
||||
|
||||
def _format_subheadings(self, subheadings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Format and validate generated subheadings."""
|
||||
formatted = []
|
||||
for subheading in subheadings:
|
||||
formatted.append({
|
||||
'title': subheading.get('title', ''),
|
||||
'level': subheading.get('level', 2),
|
||||
'keywords': subheading.get('keywords', []),
|
||||
'summary': subheading.get('summary', '')
|
||||
})
|
||||
return formatted
|
||||
|
||||
def _format_key_points(self, points: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Format and validate generated key points."""
|
||||
formatted = []
|
||||
for point in points:
|
||||
formatted.append({
|
||||
'point': point.get('point', ''),
|
||||
'importance': point.get('importance', 'medium'),
|
||||
'supporting_evidence': point.get('evidence', []),
|
||||
'related_keywords': point.get('keywords', [])
|
||||
})
|
||||
return formatted
|
||||
|
||||
def _format_content_flow(self, flow: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Format and validate generated content flow."""
|
||||
return {
|
||||
'introduction': flow.get('introduction', {}),
|
||||
'main_sections': flow.get('main_sections', []),
|
||||
'conclusion': flow.get('conclusion', {}),
|
||||
'transitions': flow.get('transitions', []),
|
||||
'content_pacing': flow.get('pacing', {})
|
||||
}
|
||||
|
||||
def generate_ai_suggestions(
|
||||
self,
|
||||
content_type: str,
|
||||
topic: str,
|
||||
audience: str,
|
||||
goals: List[str],
|
||||
tone: str,
|
||||
length: str,
|
||||
model_settings: Dict[str, Any],
|
||||
style_preferences: List[str],
|
||||
seo_preferences: Dict[str, Any],
|
||||
platform_settings: Dict[str, Any],
|
||||
platform: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate AI content suggestions based on input parameters.
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Generating AI suggestions for topic: {topic}")
|
||||
|
||||
# Create a comprehensive prompt for content generation
|
||||
prompt = f"""Generate content suggestions for the following parameters:
|
||||
|
||||
Content Type: {content_type}
|
||||
Topic: {topic}
|
||||
Target Audience: {audience}
|
||||
Goals: {', '.join(goals)}
|
||||
Tone: {tone}
|
||||
Length: {length}
|
||||
|
||||
Style Preferences:
|
||||
- Creativity Level: {model_settings.get('Creativity Level', 'medium')}
|
||||
- Formality Level: {model_settings.get('Formality Level', 'professional')}
|
||||
- Style Elements: {', '.join(style_preferences)}
|
||||
|
||||
SEO Preferences:
|
||||
- Keyword Density: {seo_preferences.get('Keyword Density', 2)}%
|
||||
- Internal Linking: {'Enabled' if seo_preferences.get('Internal Linking', True) else 'Disabled'}
|
||||
- External Linking: {'Enabled' if seo_preferences.get('External Linking', True) else 'Disabled'}
|
||||
|
||||
Platform Settings:
|
||||
- Platform: {platform}
|
||||
- Platform-specific requirements: {', '.join(platform_settings)}
|
||||
|
||||
Please generate 3 different content suggestions. Format your response as a valid JSON object with the following structure:
|
||||
{{
|
||||
"suggestions": [
|
||||
{{
|
||||
"title": "string",
|
||||
"introduction": "string",
|
||||
"key_points": ["string"],
|
||||
"main_sections": [
|
||||
{{
|
||||
"title": "string",
|
||||
"content": "string",
|
||||
"engagement_elements": ["string"],
|
||||
"seo_elements": ["string"]
|
||||
}}
|
||||
],
|
||||
"conclusion": "string",
|
||||
"seo_elements": ["string"],
|
||||
"platform_optimizations": ["string"],
|
||||
"engagement_strategies": ["string"],
|
||||
"content_metrics": {{
|
||||
"estimated_read_time": "string",
|
||||
"word_count": "number",
|
||||
"keyword_density": "number",
|
||||
"engagement_score": "number"
|
||||
}}
|
||||
}}
|
||||
]
|
||||
}}
|
||||
|
||||
IMPORTANT: Your response must be a valid JSON object. Do not include any text before or after the JSON object."""
|
||||
|
||||
# Generate content using llm_text_gen
|
||||
generated_content = llm_text_gen(
|
||||
prompt=prompt,
|
||||
max_tokens=1000,
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
frequency_penalty=0.5,
|
||||
presence_penalty=0.5
|
||||
)
|
||||
|
||||
if not generated_content:
|
||||
self.logger.error("No content generated from AI model")
|
||||
return []
|
||||
|
||||
# Parse the generated content
|
||||
try:
|
||||
# If generated_content is already a dict, use it directly
|
||||
if isinstance(generated_content, dict):
|
||||
content_data = generated_content
|
||||
else:
|
||||
# Try to parse as JSON string
|
||||
content_data = json.loads(generated_content)
|
||||
|
||||
if not content_data or 'suggestions' not in content_data:
|
||||
self.logger.error("Invalid content structure in AI response")
|
||||
return []
|
||||
|
||||
return self._format_suggestions(
|
||||
content_data,
|
||||
content_type,
|
||||
audience,
|
||||
goals,
|
||||
tone,
|
||||
length,
|
||||
model_settings,
|
||||
seo_preferences,
|
||||
platform
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Error parsing generated content: {str(e)}")
|
||||
# Try to extract JSON from the response if it's wrapped in other text
|
||||
try:
|
||||
# Find the first '{' and last '}'
|
||||
start = generated_content.find('{')
|
||||
end = generated_content.rfind('}') + 1
|
||||
if start >= 0 and end > start:
|
||||
json_str = generated_content[start:end]
|
||||
content_data = json.loads(json_str)
|
||||
if not content_data or 'suggestions' not in content_data:
|
||||
self.logger.error("Invalid content structure in extracted JSON")
|
||||
return []
|
||||
return self._format_suggestions(
|
||||
content_data,
|
||||
content_type,
|
||||
audience,
|
||||
goals,
|
||||
tone,
|
||||
length,
|
||||
model_settings,
|
||||
seo_preferences,
|
||||
platform
|
||||
)
|
||||
except Exception as e2:
|
||||
self.logger.error(f"Error extracting JSON from response: {str(e2)}")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating AI suggestions: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def _format_suggestions(
|
||||
self,
|
||||
content_data: Dict[str, Any],
|
||||
content_type: str,
|
||||
audience: str,
|
||||
goals: List[str],
|
||||
tone: str,
|
||||
length: str,
|
||||
model_settings: Dict[str, Any],
|
||||
seo_preferences: Dict[str, Any],
|
||||
platform: str
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Format and process suggestions from content data."""
|
||||
suggestions = []
|
||||
for suggestion in content_data.get('suggestions', []):
|
||||
formatted_suggestion = {
|
||||
'title': suggestion.get('title', ''),
|
||||
'type': content_type,
|
||||
'platform': platform,
|
||||
'audience': audience,
|
||||
'impact': f"High impact for {', '.join(goals)}",
|
||||
'preview': suggestion.get('introduction', ''),
|
||||
'style_elements': [
|
||||
f"Tone: {tone}",
|
||||
f"Length: {length}",
|
||||
f"Creativity: {model_settings['Creativity Level']}",
|
||||
f"Formality: {model_settings['Formality Level']}"
|
||||
],
|
||||
'seo_elements': [
|
||||
f"Keyword Density: {seo_preferences['Keyword Density']}%",
|
||||
"Internal Linking: Enabled" if seo_preferences['Internal Linking'] else "Internal Linking: Disabled",
|
||||
"External Linking: Enabled" if seo_preferences['External Linking'] else "External Linking: Disabled"
|
||||
],
|
||||
'engagement_score': f"{85 + len(suggestions)*5}%",
|
||||
'reach': 'High',
|
||||
'conversion': f"{3.5 + len(suggestions)*0.5}%",
|
||||
'seo_impact': 'Strong',
|
||||
'platform_optimizations': suggestion.get('platform_optimizations', []),
|
||||
'variations': [
|
||||
"Alternative headline",
|
||||
"Different content angle",
|
||||
"Alternative format"
|
||||
],
|
||||
'seo_recommendations': suggestion.get('seo_elements', []),
|
||||
'media_suggestions': [
|
||||
"Featured image",
|
||||
"Supporting graphics",
|
||||
"Social media visuals"
|
||||
]
|
||||
}
|
||||
suggestions.append(formatted_suggestion)
|
||||
return suggestions
|
||||
@@ -1,163 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
from lib.database.models import ContentItem, ContentType, Platform, get_engine, get_session, init_db
|
||||
from ..integrations.seo_tools import SEOToolsIntegration
|
||||
from ..integrations.gap_analyzer import GapAnalyzerIntegration
|
||||
from ..utils.date_utils import calculate_publish_dates
|
||||
from ..utils.error_handling import handle_calendar_error
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('content_calendar_debug.log', mode='a')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
class CalendarManager:
|
||||
"""
|
||||
Main calendar management system that coordinates content planning,
|
||||
scheduling, and optimization.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.manager')
|
||||
self.logger.info("Initializing CalendarManager")
|
||||
self.seo_tools = SEOToolsIntegration()
|
||||
self.gap_analyzer = GapAnalyzerIntegration()
|
||||
self.logger.info("CalendarManager initialized successfully")
|
||||
|
||||
@handle_calendar_error
|
||||
def create_calendar(
|
||||
self,
|
||||
start_date: datetime,
|
||||
duration: str, # 'weekly', 'monthly', 'quarterly'
|
||||
platforms: List[str],
|
||||
website_url: str
|
||||
) -> List[ContentItem]:
|
||||
self.logger.info(f"Creating new calendar for {website_url}")
|
||||
self.logger.debug(f"Parameters: start_date={start_date}, duration={duration}, platforms={platforms}")
|
||||
try:
|
||||
gap_analysis = self.gap_analyzer.analyze_gaps(website_url)
|
||||
topics = self._generate_topics(gap_analysis, platforms)
|
||||
schedule = calculate_publish_dates(
|
||||
topics=topics,
|
||||
start_date=start_date,
|
||||
duration=duration
|
||||
)
|
||||
# Add to DB
|
||||
for topic in schedule:
|
||||
session.add(topic)
|
||||
session.commit()
|
||||
self.logger.info("Calendar created and content scheduled in DB successfully")
|
||||
return schedule
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating calendar: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _generate_topics(
|
||||
self,
|
||||
gap_analysis: Dict[str, Any],
|
||||
platforms: List[str]
|
||||
) -> List[ContentItem]:
|
||||
topics = []
|
||||
for gap in gap_analysis['gaps']:
|
||||
topic = self._generate_topic_from_gap(gap, platforms)
|
||||
optimized_topic = self._optimize_topic(topic)
|
||||
topics.append(optimized_topic)
|
||||
return topics
|
||||
|
||||
def _generate_topic_from_gap(
|
||||
self,
|
||||
gap: Dict[str, Any],
|
||||
platforms: List[str]
|
||||
) -> ContentItem:
|
||||
topic_data = {
|
||||
'title': self._generate_title(gap),
|
||||
'description': self._generate_description(gap),
|
||||
'keywords': gap.get('keywords', []),
|
||||
'platforms': platforms,
|
||||
'content_type': self._determine_content_type(gap, platforms),
|
||||
'publish_date': datetime.now(),
|
||||
'status': 'Draft',
|
||||
'author': None,
|
||||
'tags': [],
|
||||
'notes': None,
|
||||
'seo_data': {}
|
||||
}
|
||||
return ContentItem(**topic_data)
|
||||
|
||||
def _optimize_topic(self, topic: ContentItem) -> ContentItem:
|
||||
topic.title = self.seo_tools.optimize_title(topic.title)
|
||||
topic.seo_data['meta_description'] = self.seo_tools.generate_meta_description(topic.description)
|
||||
topic.seo_data['structured_data'] = self.seo_tools.generate_structured_data(topic.content_type)
|
||||
return topic
|
||||
|
||||
def get_all_content(self) -> List[ContentItem]:
|
||||
return session.query(ContentItem).all()
|
||||
|
||||
def remove_content(self, content_id):
|
||||
content = session.query(ContentItem).get(content_id)
|
||||
if content:
|
||||
session.delete(content)
|
||||
session.commit()
|
||||
|
||||
def update_content(self, content_id, **kwargs):
|
||||
content = session.query(ContentItem).get(content_id)
|
||||
if content:
|
||||
for key, value in kwargs.items():
|
||||
setattr(content, key, value)
|
||||
session.commit()
|
||||
|
||||
def get_calendar(self) -> Optional[List[ContentItem]]:
|
||||
"""
|
||||
Get the current calendar.
|
||||
"""
|
||||
self.logger.debug("Getting current calendar")
|
||||
return self.get_all_content()
|
||||
|
||||
def update_calendar(self, calendar: List[ContentItem]) -> None:
|
||||
"""
|
||||
Update the current calendar.
|
||||
"""
|
||||
self.get_all_content()
|
||||
for content in calendar:
|
||||
session.add(content)
|
||||
session.commit()
|
||||
|
||||
def export_calendar(self) -> Optional[Dict[str, Any]]:
|
||||
"""Export the current calendar."""
|
||||
self.logger.info("Exporting calendar")
|
||||
calendar = self.get_calendar()
|
||||
if not calendar:
|
||||
self.logger.warning("No calendar to export")
|
||||
return None
|
||||
|
||||
try:
|
||||
calendar_data = [content.to_dict() for content in calendar]
|
||||
self.logger.info("Calendar exported successfully")
|
||||
return calendar_data
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error exporting calendar: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def save_calendar_to_json(self):
|
||||
calendar = self.get_calendar()
|
||||
if calendar:
|
||||
with open("calendar_data.json", "w") as f:
|
||||
json.dump(calendar, f, indent=2, default=str)
|
||||
|
||||
def load_calendar_from_json(self):
|
||||
if os.path.exists("calendar_data.json"):
|
||||
with open("calendar_data.json", "r") as f:
|
||||
data = json.load(f)
|
||||
self.update_calendar(data)
|
||||
@@ -1,151 +0,0 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
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)
|
||||
if parent_dir not in sys.path:
|
||||
sys.path.append(parent_dir)
|
||||
|
||||
from lib.database.models import ContentType, ContentItem, Platform
|
||||
from lib.ai_seo_tools.content_calendar.utils.error_handling import handle_calendar_error
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from .ai_generator import AIGenerator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentBriefGenerator:
|
||||
"""
|
||||
Generates comprehensive content briefs using AI-powered analysis.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.content_brief')
|
||||
self.logger.info("Initializing ContentBriefGenerator")
|
||||
self._setup_logging()
|
||||
self._load_ai_tools()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Configure logging for content brief generator."""
|
||||
logger.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
def _load_ai_tools(self):
|
||||
"""Load and initialize AI tools."""
|
||||
try:
|
||||
# Initialize AI tools
|
||||
self.gap_analyzer = ContentGapAnalysis()
|
||||
self.title_generator = ai_title_generator
|
||||
self.meta_generator = metadesc_generator_main
|
||||
self.ai_generator = AIGenerator()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading AI tools: {str(e)}")
|
||||
raise
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_brief(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_audience: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate a comprehensive content brief.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate brief for
|
||||
target_audience: Optional target audience data
|
||||
|
||||
Returns:
|
||||
Dictionary containing the content brief
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Generating content brief for: {content_item.title}")
|
||||
|
||||
# Generate content outline
|
||||
outline = self._generate_outline(content_item)
|
||||
|
||||
# Generate key points
|
||||
key_points = self.ai_generator.generate_key_points(
|
||||
title=content_item.title,
|
||||
content_type=content_item.content_type,
|
||||
context=content_item.context
|
||||
)
|
||||
|
||||
# Generate content flow
|
||||
content_flow = self.ai_generator.generate_content_flow(
|
||||
title=content_item.title,
|
||||
content_type=content_item.content_type,
|
||||
outline=outline
|
||||
)
|
||||
|
||||
# Compile the brief
|
||||
brief = {
|
||||
'title': content_item.title,
|
||||
'content_type': content_item.content_type.value,
|
||||
'outline': outline,
|
||||
'key_points': key_points,
|
||||
'content_flow': content_flow,
|
||||
'target_audience': target_audience or {},
|
||||
'seo_data': content_item.seo_data,
|
||||
'platform_specs': content_item.platform_specs
|
||||
}
|
||||
|
||||
logger.info("Content brief generated successfully")
|
||||
return brief
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content brief: {str(e)}")
|
||||
raise
|
||||
|
||||
def _generate_outline(
|
||||
self,
|
||||
content_item: ContentItem
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content outline with headings and subheadings.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate outline for
|
||||
|
||||
Returns:
|
||||
Dictionary containing the content outline
|
||||
"""
|
||||
try:
|
||||
# Generate main headings
|
||||
main_headings = self.ai_generator.generate_headings(
|
||||
title=content_item.title,
|
||||
content_type=content_item.content_type,
|
||||
context=content_item.context
|
||||
)
|
||||
|
||||
# Generate subheadings for each main heading
|
||||
subheadings = {}
|
||||
for heading in main_headings:
|
||||
heading_subheadings = self.ai_generator.generate_subheadings(
|
||||
main_heading=heading,
|
||||
content_type=content_item.content_type,
|
||||
context=content_item.context
|
||||
)
|
||||
subheadings[heading['title']] = heading_subheadings
|
||||
|
||||
return {
|
||||
'main_headings': main_headings,
|
||||
'subheadings': subheadings
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating outline: {str(e)}")
|
||||
return {
|
||||
'main_headings': [],
|
||||
'subheadings': {}
|
||||
}
|
||||
@@ -1,626 +0,0 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 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
|
||||
from ..utils.error_handling import handle_calendar_error
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.content_calendar.core.content_repurposer import SmartContentRepurposingEngine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentGenerator:
|
||||
"""
|
||||
Enhanced content generator with smart repurposing capabilities.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.content_generator')
|
||||
self.logger.info("Initializing ContentGenerator")
|
||||
self._setup_logging()
|
||||
self._load_ai_tools()
|
||||
# Initialize the Smart Content Repurposing Engine
|
||||
self.repurposing_engine = SmartContentRepurposingEngine()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Configure logging for content generator."""
|
||||
logger.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
def _load_ai_tools(self):
|
||||
"""Load and initialize AI tools."""
|
||||
try:
|
||||
# Initialize AI tools
|
||||
self.gap_analyzer = ContentGapAnalysis()
|
||||
self.title_generator = ai_title_generator
|
||||
self.meta_generator = metadesc_generator_main
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading AI tools: {str(e)}")
|
||||
raise
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_headings(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate main headings for content.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate headings for
|
||||
context: Content context from gap analysis
|
||||
|
||||
Returns:
|
||||
List of main headings with metadata
|
||||
"""
|
||||
try:
|
||||
# Use AI to generate headings based on content type and context
|
||||
headings = self._generate_ai_headings(
|
||||
title=content_item.title,
|
||||
content_type=content_item.content_type,
|
||||
context=context
|
||||
)
|
||||
|
||||
# Format and validate headings
|
||||
formatted_headings = []
|
||||
for heading in headings:
|
||||
formatted_heading = {
|
||||
'title': heading['title'],
|
||||
'level': heading.get('level', 1),
|
||||
'keywords': heading.get('keywords', []),
|
||||
'summary': heading.get('summary', '')
|
||||
}
|
||||
formatted_headings.append(formatted_heading)
|
||||
|
||||
return formatted_headings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating headings: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_subheadings(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
main_headings: List[Dict[str, Any]],
|
||||
context: Dict[str, Any]
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Generate subheadings for each main heading.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate subheadings for
|
||||
main_headings: List of main headings
|
||||
context: Content context from gap analysis
|
||||
|
||||
Returns:
|
||||
Dictionary mapping main headings to their subheadings
|
||||
"""
|
||||
try:
|
||||
subheadings = {}
|
||||
|
||||
for heading in main_headings:
|
||||
# Generate subheadings for each main heading
|
||||
heading_subheadings = self._generate_ai_subheadings(
|
||||
main_heading=heading,
|
||||
content_type=content_item.content_type,
|
||||
context=context
|
||||
)
|
||||
|
||||
# Format and validate subheadings
|
||||
formatted_subheadings = []
|
||||
for subheading in heading_subheadings:
|
||||
formatted_subheading = {
|
||||
'title': subheading['title'],
|
||||
'level': subheading.get('level', 2),
|
||||
'keywords': subheading.get('keywords', []),
|
||||
'summary': subheading.get('summary', '')
|
||||
}
|
||||
formatted_subheadings.append(formatted_subheading)
|
||||
|
||||
subheadings[heading['title']] = formatted_subheadings
|
||||
|
||||
return subheadings
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating subheadings: {str(e)}")
|
||||
return {}
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_key_points(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate key points for the content.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate key points for
|
||||
context: Content context from gap analysis
|
||||
|
||||
Returns:
|
||||
List of key points with supporting information
|
||||
"""
|
||||
try:
|
||||
# Generate key points using AI
|
||||
key_points = self._generate_ai_key_points(
|
||||
title=content_item.title,
|
||||
content_type=content_item.content_type,
|
||||
context=context
|
||||
)
|
||||
|
||||
# Format and validate key points
|
||||
formatted_points = []
|
||||
for point in key_points:
|
||||
formatted_point = {
|
||||
'point': point['point'],
|
||||
'importance': point.get('importance', 'medium'),
|
||||
'supporting_evidence': point.get('evidence', []),
|
||||
'related_keywords': point.get('keywords', [])
|
||||
}
|
||||
formatted_points.append(formatted_point)
|
||||
|
||||
return formatted_points
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating key points: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_content_flow(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
outline: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content flow and structure.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate flow for
|
||||
outline: Content outline with headings and key points
|
||||
|
||||
Returns:
|
||||
Dictionary containing content flow and structure
|
||||
"""
|
||||
try:
|
||||
# Generate content flow using AI
|
||||
flow = self._generate_ai_content_flow(
|
||||
title=content_item.title,
|
||||
content_type=content_item.content_type,
|
||||
outline=outline
|
||||
)
|
||||
|
||||
return {
|
||||
'introduction': flow.get('introduction', {}),
|
||||
'main_sections': flow.get('main_sections', []),
|
||||
'conclusion': flow.get('conclusion', {}),
|
||||
'transitions': flow.get('transitions', []),
|
||||
'content_pacing': flow.get('pacing', {})
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content flow: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _generate_ai_headings(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Use AI to generate content headings.
|
||||
"""
|
||||
# TODO: Implement AI heading generation
|
||||
# This would use the existing AI tools to generate headings
|
||||
return []
|
||||
|
||||
def _generate_ai_subheadings(
|
||||
self,
|
||||
main_heading: Dict[str, Any],
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Use AI to generate subheadings.
|
||||
"""
|
||||
# TODO: Implement AI subheading generation
|
||||
return []
|
||||
|
||||
def _generate_ai_key_points(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
context: Dict[str, Any]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Use AI to generate key points.
|
||||
"""
|
||||
# TODO: Implement AI key point generation
|
||||
return []
|
||||
|
||||
def _generate_ai_content_flow(
|
||||
self,
|
||||
title: str,
|
||||
content_type: ContentType,
|
||||
outline: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Use AI to generate content flow.
|
||||
"""
|
||||
# TODO: Implement AI content flow generation
|
||||
return {}
|
||||
|
||||
def generate_variation(self, content: Dict[str, Any], variation_type: str) -> Dict[str, Any]:
|
||||
"""Generate a variation of the given content.
|
||||
|
||||
Args:
|
||||
content: Original content to vary
|
||||
variation_type: Type of variation to generate ('tone', 'length', 'style', etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary containing the varied content
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Generating {variation_type} variation for content")
|
||||
|
||||
# Generate variation based on type
|
||||
variation = {
|
||||
'title': f"{content.get('title', '')} - {variation_type.title()} Variation",
|
||||
'content_flow': {
|
||||
'introduction': {
|
||||
'summary': f"Varied introduction for {content.get('title', '')}",
|
||||
'key_points': [
|
||||
f"Varied key point 1 for {variation_type}",
|
||||
f"Varied key point 2 for {variation_type}",
|
||||
f"Varied key point 3 for {variation_type}"
|
||||
]
|
||||
},
|
||||
'main_content': {
|
||||
'sections': [
|
||||
{
|
||||
'title': f"Varied Section 1: {variation_type.title()} Approach",
|
||||
'content': f"Varied content for {variation_type}",
|
||||
'subsections': []
|
||||
},
|
||||
{
|
||||
'title': f"Varied Section 2: {variation_type.title()} Details",
|
||||
'content': "Varied details and information",
|
||||
'subsections': []
|
||||
}
|
||||
]
|
||||
},
|
||||
'conclusion': {
|
||||
'summary': f"Varied conclusion for {variation_type}",
|
||||
'call_to_action': "Varied call to action"
|
||||
}
|
||||
},
|
||||
'metadata': {
|
||||
'variation_type': variation_type,
|
||||
'original_content': content.get('title', ''),
|
||||
'platform': content.get('metadata', {}).get('platform', 'Unknown'),
|
||||
'content_type': content.get('metadata', {}).get('content_type', 'Unknown')
|
||||
}
|
||||
}
|
||||
|
||||
return variation
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating variation: {str(e)}")
|
||||
return {}
|
||||
|
||||
@handle_calendar_error
|
||||
def repurpose_content_for_platforms(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_platforms: List[Platform],
|
||||
strategy: str = 'adaptive'
|
||||
) -> List[ContentItem]:
|
||||
"""
|
||||
Repurpose existing content for multiple platforms using the Smart Content Repurposing Engine.
|
||||
|
||||
Args:
|
||||
content_item: Original content to repurpose
|
||||
target_platforms: List of platforms to create content for
|
||||
strategy: Repurposing strategy ('adaptive', 'atomic', 'series')
|
||||
|
||||
Returns:
|
||||
List of repurposed content items optimized for each platform
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Repurposing content '{content_item.title}' for {len(target_platforms)} platforms")
|
||||
|
||||
# Use the repurposing engine to create platform-specific content
|
||||
repurposed_content = self.repurposing_engine.repurpose_single_content(
|
||||
content=content_item,
|
||||
target_platforms=target_platforms,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
self.logger.info(f"Successfully created {len(repurposed_content)} repurposed content pieces")
|
||||
return repurposed_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error repurposing content: {str(e)}")
|
||||
return []
|
||||
|
||||
@handle_calendar_error
|
||||
def create_content_series_across_platforms(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform],
|
||||
series_type: str = 'progressive_disclosure'
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""
|
||||
Create a cross-platform content series with progressive disclosure strategy.
|
||||
|
||||
Args:
|
||||
source_content: Original comprehensive content
|
||||
platforms: Target platforms for the series
|
||||
series_type: Type of series ('progressive_disclosure', 'platform_native')
|
||||
|
||||
Returns:
|
||||
Dictionary mapping platforms to their content pieces
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Creating cross-platform series for '{source_content.title}'")
|
||||
|
||||
# Use the repurposing engine to create a content series
|
||||
series_content = self.repurposing_engine.create_content_series(
|
||||
content=source_content,
|
||||
platforms=platforms,
|
||||
series_type=series_type
|
||||
)
|
||||
|
||||
total_pieces = sum(len(pieces) for pieces in series_content.values())
|
||||
self.logger.info(f"Successfully created series with {total_pieces} pieces across {len(series_content)} platforms")
|
||||
|
||||
return series_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating content series: {str(e)}")
|
||||
return {}
|
||||
|
||||
@handle_calendar_error
|
||||
def analyze_content_for_repurposing(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
available_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze content and get AI-powered repurposing suggestions.
|
||||
|
||||
Args:
|
||||
content_item: Content to analyze
|
||||
available_platforms: Available platforms for repurposing
|
||||
|
||||
Returns:
|
||||
Dictionary containing repurposing suggestions and analysis
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Analyzing content '{content_item.title}' for repurposing opportunities")
|
||||
|
||||
# Get repurposing suggestions from the engine
|
||||
suggestions = self.repurposing_engine.get_repurposing_suggestions(
|
||||
content=content_item,
|
||||
available_platforms=available_platforms
|
||||
)
|
||||
|
||||
# Add content analysis
|
||||
content_text = content_item.description or content_item.notes or ""
|
||||
content_atoms = self.repurposing_engine.analyze_content_atoms(
|
||||
content=content_text,
|
||||
title=content_item.title
|
||||
)
|
||||
|
||||
analysis = {
|
||||
'content_analysis': {
|
||||
'word_count': len(content_text.split()) if content_text else 0,
|
||||
'content_richness': self._assess_content_richness(content_atoms),
|
||||
'repurposing_potential': self._assess_repurposing_potential(content_atoms),
|
||||
'content_atoms': content_atoms
|
||||
},
|
||||
'platform_suggestions': suggestions['recommended_platforms'],
|
||||
'strategy_suggestions': suggestions['repurposing_strategies'],
|
||||
'estimated_output': {
|
||||
'total_pieces': suggestions['estimated_pieces'],
|
||||
'time_savings': f"{suggestions['estimated_pieces'] * 2} hours",
|
||||
'content_multiplication': f"{suggestions['estimated_pieces']}x"
|
||||
}
|
||||
}
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error analyzing content for repurposing: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _assess_content_richness(self, content_atoms: Dict[str, List[str]]) -> str:
|
||||
"""Assess the richness of content based on extracted atoms."""
|
||||
total_atoms = sum(len(atoms) for atoms in content_atoms.values())
|
||||
|
||||
if total_atoms >= 15:
|
||||
return "High"
|
||||
elif total_atoms >= 8:
|
||||
return "Medium"
|
||||
else:
|
||||
return "Low"
|
||||
|
||||
def _assess_repurposing_potential(self, content_atoms: Dict[str, List[str]]) -> str:
|
||||
"""Assess the repurposing potential based on content atoms."""
|
||||
# Check for diverse content types
|
||||
atom_types_with_content = sum(1 for atoms in content_atoms.values() if atoms)
|
||||
|
||||
if atom_types_with_content >= 4:
|
||||
return "Excellent"
|
||||
elif atom_types_with_content >= 3:
|
||||
return "Good"
|
||||
elif atom_types_with_content >= 2:
|
||||
return "Fair"
|
||||
else:
|
||||
return "Limited"
|
||||
|
||||
@handle_calendar_error
|
||||
def generate_content_with_repurposing_plan(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
context: Dict[str, Any],
|
||||
target_platforms: List[Platform] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content along with a comprehensive repurposing plan.
|
||||
|
||||
Args:
|
||||
content_item: Content item to generate
|
||||
context: Content context from gap analysis
|
||||
target_platforms: Platforms to include in repurposing plan
|
||||
|
||||
Returns:
|
||||
Dictionary containing generated content and repurposing plan
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Generating content with repurposing plan for '{content_item.title}'")
|
||||
|
||||
# Generate the main content structure
|
||||
headings = self.generate_headings(content_item, context)
|
||||
subheadings = self.generate_subheadings(content_item, headings, context)
|
||||
key_points = self.generate_key_points(content_item, context)
|
||||
|
||||
outline = {
|
||||
'headings': headings,
|
||||
'subheadings': subheadings,
|
||||
'key_points': key_points
|
||||
}
|
||||
|
||||
content_flow = self.generate_content_flow(content_item, outline)
|
||||
|
||||
# Create repurposing plan if platforms are specified
|
||||
repurposing_plan = {}
|
||||
if target_platforms:
|
||||
# Analyze repurposing potential
|
||||
analysis = self.analyze_content_for_repurposing(content_item, target_platforms)
|
||||
|
||||
# Generate repurposing suggestions
|
||||
repurposing_plan = {
|
||||
'analysis': analysis,
|
||||
'recommended_strategy': self._recommend_repurposing_strategy(analysis),
|
||||
'platform_roadmap': self._create_platform_roadmap(content_item, target_platforms),
|
||||
'content_calendar_integration': self._suggest_calendar_integration(content_item, target_platforms)
|
||||
}
|
||||
|
||||
return {
|
||||
'content': {
|
||||
'outline': outline,
|
||||
'content_flow': content_flow,
|
||||
'metadata': {
|
||||
'generated_at': str(datetime.now()),
|
||||
'content_type': content_item.content_type.name,
|
||||
'platforms': [p.name for p in content_item.platforms] if content_item.platforms else []
|
||||
}
|
||||
},
|
||||
'repurposing_plan': repurposing_plan
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating content with repurposing plan: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _recommend_repurposing_strategy(self, analysis: Dict[str, Any]) -> str:
|
||||
"""Recommend the best repurposing strategy based on content analysis."""
|
||||
content_richness = analysis.get('content_analysis', {}).get('content_richness', 'Low')
|
||||
repurposing_potential = analysis.get('content_analysis', {}).get('repurposing_potential', 'Limited')
|
||||
|
||||
if content_richness == 'High' and repurposing_potential in ['Excellent', 'Good']:
|
||||
return 'progressive_disclosure'
|
||||
elif content_richness in ['Medium', 'High']:
|
||||
return 'adaptive'
|
||||
else:
|
||||
return 'atomic'
|
||||
|
||||
def _create_platform_roadmap(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a roadmap for content distribution across platforms."""
|
||||
roadmap = {
|
||||
'timeline': {},
|
||||
'platform_sequence': [],
|
||||
'cross_promotion_opportunities': []
|
||||
}
|
||||
|
||||
# Create a timeline for content release
|
||||
base_date = content_item.publish_date or datetime.now()
|
||||
|
||||
for i, platform in enumerate(target_platforms):
|
||||
release_date = base_date + timedelta(days=i)
|
||||
roadmap['timeline'][platform.name] = {
|
||||
'release_date': release_date.strftime('%Y-%m-%d'),
|
||||
'content_type': self._get_optimal_content_type_for_platform(platform),
|
||||
'engagement_strategy': self._get_engagement_strategy_for_platform(platform)
|
||||
}
|
||||
roadmap['platform_sequence'].append(platform.name)
|
||||
|
||||
return roadmap
|
||||
|
||||
def _suggest_calendar_integration(
|
||||
self,
|
||||
content_item: ContentItem,
|
||||
target_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""Suggest how to integrate repurposed content into the content calendar."""
|
||||
return {
|
||||
'scheduling_recommendations': {
|
||||
'primary_content': 'Schedule as main content piece',
|
||||
'repurposed_content': 'Schedule 1-2 days after primary content',
|
||||
'series_content': 'Schedule weekly releases for maximum impact'
|
||||
},
|
||||
'calendar_tags': [
|
||||
'repurposed_content',
|
||||
f'source_{content_item.id}',
|
||||
'multi_platform_series'
|
||||
],
|
||||
'performance_tracking': {
|
||||
'metrics_to_track': ['engagement_rate', 'cross_platform_traffic', 'conversion_rate'],
|
||||
'comparison_baseline': 'Compare against single-platform content performance'
|
||||
}
|
||||
}
|
||||
|
||||
def _get_optimal_content_type_for_platform(self, platform: Platform) -> str:
|
||||
"""Get the optimal content type for a specific platform."""
|
||||
platform_content_types = {
|
||||
Platform.TWITTER: 'Thread or single tweet',
|
||||
Platform.LINKEDIN: 'Professional post or article',
|
||||
Platform.INSTAGRAM: 'Visual post with caption',
|
||||
Platform.FACEBOOK: 'Engaging post with discussion starter',
|
||||
Platform.WEBSITE: 'Full blog post or article'
|
||||
}
|
||||
return platform_content_types.get(platform, 'Standard post')
|
||||
|
||||
def _get_engagement_strategy_for_platform(self, platform: Platform) -> str:
|
||||
"""Get the engagement strategy for a specific platform."""
|
||||
engagement_strategies = {
|
||||
Platform.TWITTER: 'Use hashtags, engage in conversations, create polls',
|
||||
Platform.LINKEDIN: 'Professional networking, thought leadership, industry discussions',
|
||||
Platform.INSTAGRAM: 'Visual storytelling, user-generated content, stories',
|
||||
Platform.FACEBOOK: 'Community building, discussions, live interactions',
|
||||
Platform.WEBSITE: 'SEO optimization, internal linking, lead magnets'
|
||||
}
|
||||
return engagement_strategies.get(platform, 'Standard engagement tactics')
|
||||
@@ -1,599 +0,0 @@
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import json
|
||||
|
||||
# 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, SEOData
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from ..utils.error_handling import handle_calendar_error
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentAtomizer:
|
||||
"""
|
||||
Break down content into atomic pieces that can be recombined
|
||||
for different platforms and purposes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.atomizer')
|
||||
|
||||
def atomize_content(self, content: str, title: str = "") -> Dict[str, List[str]]:
|
||||
"""
|
||||
Extract key quotes, statistics, tips, and examples from content.
|
||||
|
||||
Args:
|
||||
content: The content text to atomize
|
||||
title: The content title for context
|
||||
|
||||
Returns:
|
||||
Dictionary containing different types of content atoms
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Atomizing content: {title[:50]}...")
|
||||
|
||||
# Use AI to extract content atoms
|
||||
prompt = f"""
|
||||
Analyze the following content and extract key elements that can be repurposed:
|
||||
|
||||
Title: {title}
|
||||
Content: {content[:3000]}...
|
||||
|
||||
Extract and categorize the following elements:
|
||||
1. Key Statistics (numbers, percentages, data points)
|
||||
2. Quotable Insights (memorable quotes or key insights)
|
||||
3. Actionable Tips (practical advice or steps)
|
||||
4. Examples/Case Studies (real examples or stories)
|
||||
5. Key Questions (thought-provoking questions)
|
||||
6. Main Arguments (core points or arguments)
|
||||
|
||||
Format your response as JSON:
|
||||
{{
|
||||
"statistics": ["stat1", "stat2", ...],
|
||||
"quotes": ["quote1", "quote2", ...],
|
||||
"tips": ["tip1", "tip2", ...],
|
||||
"examples": ["example1", "example2", ...],
|
||||
"questions": ["question1", "question2", ...],
|
||||
"arguments": ["argument1", "argument2", ...]
|
||||
}}
|
||||
"""
|
||||
|
||||
response = llm_text_gen(
|
||||
prompt=prompt,
|
||||
system_prompt="You are an expert content analyst. Extract key elements that can be repurposed across different platforms.",
|
||||
json_struct={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"statistics": {"type": "array", "items": {"type": "string"}},
|
||||
"quotes": {"type": "array", "items": {"type": "string"}},
|
||||
"tips": {"type": "array", "items": {"type": "string"}},
|
||||
"examples": {"type": "array", "items": {"type": "string"}},
|
||||
"questions": {"type": "array", "items": {"type": "string"}},
|
||||
"arguments": {"type": "array", "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if response:
|
||||
return response
|
||||
else:
|
||||
# Fallback to basic extraction
|
||||
return self._basic_content_extraction(content)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error atomizing content: {str(e)}")
|
||||
return self._basic_content_extraction(content)
|
||||
|
||||
def _basic_content_extraction(self, content: str) -> Dict[str, List[str]]:
|
||||
"""Fallback method for basic content extraction."""
|
||||
atoms = {
|
||||
"statistics": [],
|
||||
"quotes": [],
|
||||
"tips": [],
|
||||
"examples": [],
|
||||
"questions": [],
|
||||
"arguments": []
|
||||
}
|
||||
|
||||
# Extract statistics (numbers with %)
|
||||
stats = re.findall(r'\d+%|\d+\.\d+%|\d+,\d+|\d+ percent', content)
|
||||
atoms["statistics"] = stats[:5] # Limit to 5
|
||||
|
||||
# Extract questions
|
||||
questions = re.findall(r'[A-Z][^.!?]*\?', content)
|
||||
atoms["questions"] = questions[:3] # Limit to 3
|
||||
|
||||
# Extract sentences that might be tips (containing words like "should", "must", "need to")
|
||||
tip_patterns = r'[^.!?]*(?:should|must|need to|important to|remember to)[^.!?]*[.!?]'
|
||||
tips = re.findall(tip_patterns, content, re.IGNORECASE)
|
||||
atoms["tips"] = tips[:5] # Limit to 5
|
||||
|
||||
return atoms
|
||||
|
||||
class ContentRepurposer:
|
||||
"""
|
||||
Main content repurposing engine that transforms content for different platforms.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.repurposer')
|
||||
self.atomizer = ContentAtomizer()
|
||||
|
||||
# Platform-specific content specifications
|
||||
self.platform_specs = {
|
||||
Platform.TWITTER: {
|
||||
'max_length': 280,
|
||||
'optimal_length': 240,
|
||||
'format': 'concise',
|
||||
'tone': 'engaging',
|
||||
'hashtags': True,
|
||||
'mentions': True
|
||||
},
|
||||
Platform.LINKEDIN: {
|
||||
'max_length': 3000,
|
||||
'optimal_length': 1500,
|
||||
'format': 'professional',
|
||||
'tone': 'authoritative',
|
||||
'hashtags': True,
|
||||
'mentions': False
|
||||
},
|
||||
Platform.INSTAGRAM: {
|
||||
'max_length': 2200,
|
||||
'optimal_length': 1000,
|
||||
'format': 'visual-focused',
|
||||
'tone': 'casual',
|
||||
'hashtags': True,
|
||||
'mentions': True
|
||||
},
|
||||
Platform.FACEBOOK: {
|
||||
'max_length': 63206,
|
||||
'optimal_length': 500,
|
||||
'format': 'engaging',
|
||||
'tone': 'conversational',
|
||||
'hashtags': False,
|
||||
'mentions': True
|
||||
},
|
||||
Platform.WEBSITE: {
|
||||
'max_length': None,
|
||||
'optimal_length': 2000,
|
||||
'format': 'comprehensive',
|
||||
'tone': 'informative',
|
||||
'hashtags': False,
|
||||
'mentions': False
|
||||
}
|
||||
}
|
||||
|
||||
@handle_calendar_error
|
||||
def repurpose_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
target_platforms: List[Platform],
|
||||
repurpose_strategy: str = 'adaptive'
|
||||
) -> List[ContentItem]:
|
||||
"""
|
||||
Repurpose content for multiple platforms.
|
||||
|
||||
Args:
|
||||
source_content: Original content to repurpose
|
||||
target_platforms: List of platforms to create content for
|
||||
repurpose_strategy: Strategy for repurposing ('adaptive', 'atomic', 'series')
|
||||
|
||||
Returns:
|
||||
List of repurposed content items
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Repurposing content '{source_content.title}' for {len(target_platforms)} platforms")
|
||||
|
||||
repurposed_content = []
|
||||
|
||||
# Get content text (assuming it's in description or notes)
|
||||
content_text = source_content.description or source_content.notes or ""
|
||||
|
||||
if not content_text:
|
||||
self.logger.warning("No content text found for repurposing")
|
||||
return []
|
||||
|
||||
# Atomize the content
|
||||
atoms = self.atomizer.atomize_content(content_text, source_content.title)
|
||||
|
||||
# Generate repurposed content for each platform
|
||||
for platform in target_platforms:
|
||||
if platform == source_content.platforms[0] if source_content.platforms else None:
|
||||
continue # Skip the original platform
|
||||
|
||||
repurposed_item = self._create_platform_specific_content(
|
||||
source_content=source_content,
|
||||
target_platform=platform,
|
||||
atoms=atoms,
|
||||
strategy=repurpose_strategy
|
||||
)
|
||||
|
||||
if repurposed_item:
|
||||
repurposed_content.append(repurposed_item)
|
||||
|
||||
self.logger.info(f"Successfully repurposed content into {len(repurposed_content)} variations")
|
||||
return repurposed_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error repurposing content: {str(e)}")
|
||||
return []
|
||||
|
||||
def _create_platform_specific_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
target_platform: Platform,
|
||||
atoms: Dict[str, List[str]],
|
||||
strategy: str
|
||||
) -> Optional[ContentItem]:
|
||||
"""Create platform-specific content variation."""
|
||||
try:
|
||||
platform_spec = self.platform_specs.get(target_platform, {})
|
||||
|
||||
# Generate platform-specific content using AI
|
||||
repurposed_text = self._generate_platform_content(
|
||||
source_content=source_content,
|
||||
target_platform=target_platform,
|
||||
atoms=atoms,
|
||||
platform_spec=platform_spec,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
if not repurposed_text:
|
||||
return None
|
||||
|
||||
# Create new content item
|
||||
repurposed_item = ContentItem(
|
||||
title=self._adapt_title_for_platform(source_content.title, target_platform),
|
||||
description=repurposed_text,
|
||||
content_type=self._determine_content_type_for_platform(target_platform),
|
||||
platforms=[target_platform],
|
||||
publish_date=source_content.publish_date + timedelta(days=1), # Schedule for next day
|
||||
status="draft",
|
||||
author=source_content.author,
|
||||
tags=source_content.tags + [f"repurposed_from_{source_content.id}"],
|
||||
notes=f"Repurposed from: {source_content.title}",
|
||||
seo_data=self._adapt_seo_data_for_platform(source_content.seo_data, target_platform)
|
||||
)
|
||||
|
||||
return repurposed_item
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating platform-specific content: {str(e)}")
|
||||
return None
|
||||
|
||||
def _generate_platform_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
target_platform: Platform,
|
||||
atoms: Dict[str, List[str]],
|
||||
platform_spec: Dict[str, Any],
|
||||
strategy: str
|
||||
) -> str:
|
||||
"""Generate content optimized for specific platform."""
|
||||
try:
|
||||
# Prepare content elements
|
||||
title = source_content.title
|
||||
original_content = source_content.description or ""
|
||||
|
||||
# Create platform-specific prompt
|
||||
prompt = self._create_repurposing_prompt(
|
||||
title=title,
|
||||
original_content=original_content,
|
||||
target_platform=target_platform,
|
||||
atoms=atoms,
|
||||
platform_spec=platform_spec,
|
||||
strategy=strategy
|
||||
)
|
||||
|
||||
# Generate content using AI
|
||||
repurposed_content = llm_text_gen(prompt)
|
||||
|
||||
return repurposed_content or ""
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error generating platform content: {str(e)}")
|
||||
return ""
|
||||
|
||||
def _create_repurposing_prompt(
|
||||
self,
|
||||
title: str,
|
||||
original_content: str,
|
||||
target_platform: Platform,
|
||||
atoms: Dict[str, List[str]],
|
||||
platform_spec: Dict[str, Any],
|
||||
strategy: str
|
||||
) -> str:
|
||||
"""Create AI prompt for content repurposing."""
|
||||
|
||||
platform_guidelines = {
|
||||
Platform.TWITTER: "Create engaging tweets that drive conversation. Use threads for complex topics. Include relevant hashtags.",
|
||||
Platform.LINKEDIN: "Write professional content that provides value to business professionals. Focus on insights and actionable advice.",
|
||||
Platform.INSTAGRAM: "Create visually-oriented content with engaging captions. Use storytelling and include relevant hashtags.",
|
||||
Platform.FACEBOOK: "Write conversational content that encourages engagement. Ask questions and create community discussion.",
|
||||
Platform.WEBSITE: "Create comprehensive, SEO-optimized content with clear structure and valuable information."
|
||||
}
|
||||
|
||||
atoms_text = ""
|
||||
for atom_type, atom_list in atoms.items():
|
||||
if atom_list:
|
||||
atoms_text += f"\n{atom_type.title()}: {', '.join(atom_list[:3])}"
|
||||
|
||||
prompt = f"""
|
||||
Repurpose the following content for {target_platform.name}:
|
||||
|
||||
Original Title: {title}
|
||||
Original Content: {original_content[:1500]}...
|
||||
|
||||
Key Content Elements:{atoms_text}
|
||||
|
||||
Platform Guidelines: {platform_guidelines.get(target_platform, '')}
|
||||
|
||||
Platform Specifications:
|
||||
- Optimal Length: {platform_spec.get('optimal_length', 'flexible')} characters
|
||||
- Format: {platform_spec.get('format', 'standard')}
|
||||
- Tone: {platform_spec.get('tone', 'professional')}
|
||||
- Include Hashtags: {platform_spec.get('hashtags', False)}
|
||||
|
||||
Requirements:
|
||||
1. Adapt the content to fit {target_platform.name}'s format and audience
|
||||
2. Maintain the core message and value
|
||||
3. Optimize for {target_platform.name} engagement
|
||||
4. Include platform-appropriate calls to action
|
||||
5. Use the extracted content elements effectively
|
||||
|
||||
Create compelling, platform-optimized content that will perform well on {target_platform.name}.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _adapt_title_for_platform(self, original_title: str, platform: Platform) -> str:
|
||||
"""Adapt title for specific platform."""
|
||||
platform_prefixes = {
|
||||
Platform.TWITTER: "🧵 ",
|
||||
Platform.LINKEDIN: "💼 ",
|
||||
Platform.INSTAGRAM: "📸 ",
|
||||
Platform.FACEBOOK: "💬 ",
|
||||
Platform.WEBSITE: ""
|
||||
}
|
||||
|
||||
prefix = platform_prefixes.get(platform, "")
|
||||
return f"{prefix}{original_title}"
|
||||
|
||||
def _determine_content_type_for_platform(self, platform: Platform) -> ContentType:
|
||||
"""Determine appropriate content type for platform."""
|
||||
platform_content_types = {
|
||||
Platform.TWITTER: ContentType.SOCIAL_MEDIA,
|
||||
Platform.LINKEDIN: ContentType.SOCIAL_MEDIA,
|
||||
Platform.INSTAGRAM: ContentType.SOCIAL_MEDIA,
|
||||
Platform.FACEBOOK: ContentType.SOCIAL_MEDIA,
|
||||
Platform.WEBSITE: ContentType.BLOG_POST
|
||||
}
|
||||
|
||||
return platform_content_types.get(platform, ContentType.SOCIAL_MEDIA)
|
||||
|
||||
def _adapt_seo_data_for_platform(self, original_seo: SEOData, platform: Platform) -> SEOData:
|
||||
"""Adapt SEO data for specific platform."""
|
||||
if platform == Platform.WEBSITE:
|
||||
return original_seo
|
||||
|
||||
# For social media platforms, create simplified SEO data
|
||||
return SEOData(
|
||||
title=original_seo.title,
|
||||
meta_description=original_seo.meta_description[:160] if original_seo.meta_description else "",
|
||||
keywords=original_seo.keywords[:5] if original_seo.keywords else [],
|
||||
structured_data={}
|
||||
)
|
||||
|
||||
class ContentSeriesRepurposer:
|
||||
"""
|
||||
Create cross-platform content series with progressive disclosure strategy.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.series_repurposer')
|
||||
self.repurposer = ContentRepurposer()
|
||||
|
||||
def create_cross_platform_series(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform],
|
||||
series_strategy: str = 'progressive_disclosure'
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""
|
||||
Create a content series that progressively reveals information
|
||||
across different platforms, driving traffic between them.
|
||||
|
||||
Args:
|
||||
source_content: Original comprehensive content
|
||||
platforms: Target platforms for the series
|
||||
series_strategy: Strategy for content distribution
|
||||
|
||||
Returns:
|
||||
Dictionary mapping platforms to their content pieces
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Creating cross-platform series for: {source_content.title}")
|
||||
|
||||
series_content = {}
|
||||
|
||||
if series_strategy == 'progressive_disclosure':
|
||||
series_content = self._create_progressive_disclosure_series(
|
||||
source_content, platforms
|
||||
)
|
||||
elif series_strategy == 'platform_native':
|
||||
series_content = self._create_platform_native_series(
|
||||
source_content, platforms
|
||||
)
|
||||
else:
|
||||
# Default to simple repurposing
|
||||
repurposed = self.repurposer.repurpose_content(
|
||||
source_content, platforms
|
||||
)
|
||||
for item in repurposed:
|
||||
platform = item.platforms[0]
|
||||
if platform not in series_content:
|
||||
series_content[platform] = []
|
||||
series_content[platform].append(item)
|
||||
|
||||
return series_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating cross-platform series: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _create_progressive_disclosure_series(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform]
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""Create series with progressive information disclosure."""
|
||||
series_content = {}
|
||||
|
||||
# Define disclosure strategy
|
||||
disclosure_strategy = {
|
||||
Platform.TWITTER: "teaser", # Hook with key stat/question
|
||||
Platform.INSTAGRAM: "visual", # Visual summary with key points
|
||||
Platform.LINKEDIN: "insight", # Professional insight/analysis
|
||||
Platform.FACEBOOK: "discussion", # Community discussion starter
|
||||
Platform.WEBSITE: "complete" # Full detailed content
|
||||
}
|
||||
|
||||
for platform in platforms:
|
||||
strategy = disclosure_strategy.get(platform, "summary")
|
||||
content_piece = self._create_disclosure_content(
|
||||
source_content, platform, strategy
|
||||
)
|
||||
if content_piece:
|
||||
series_content[platform] = [content_piece]
|
||||
|
||||
return series_content
|
||||
|
||||
def _create_disclosure_content(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platform: Platform,
|
||||
disclosure_type: str
|
||||
) -> Optional[ContentItem]:
|
||||
"""Create content piece for specific disclosure strategy."""
|
||||
try:
|
||||
# This would use the repurposer with specific instructions
|
||||
# for the disclosure type
|
||||
repurposed = self.repurposer._create_platform_specific_content(
|
||||
source_content=source_content,
|
||||
target_platform=platform,
|
||||
atoms=self.repurposer.atomizer.atomize_content(
|
||||
source_content.description or "",
|
||||
source_content.title
|
||||
),
|
||||
strategy=disclosure_type
|
||||
)
|
||||
|
||||
return repurposed
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating disclosure content: {str(e)}")
|
||||
return None
|
||||
|
||||
def _create_platform_native_series(
|
||||
self,
|
||||
source_content: ContentItem,
|
||||
platforms: List[Platform]
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""Create series optimized for each platform's native format."""
|
||||
# Implementation for platform-native series
|
||||
# This would create multiple pieces per platform
|
||||
# optimized for that platform's specific characteristics
|
||||
return {}
|
||||
|
||||
# Main repurposing interface
|
||||
class SmartContentRepurposingEngine:
|
||||
"""
|
||||
Main interface for the Smart Content Repurposing Engine.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger('content_calendar.repurposing_engine')
|
||||
self.repurposer = ContentRepurposer()
|
||||
self.series_repurposer = ContentSeriesRepurposer()
|
||||
self.atomizer = ContentAtomizer()
|
||||
|
||||
def repurpose_single_content(
|
||||
self,
|
||||
content: ContentItem,
|
||||
target_platforms: List[Platform],
|
||||
strategy: str = 'adaptive'
|
||||
) -> List[ContentItem]:
|
||||
"""Repurpose a single piece of content."""
|
||||
return self.repurposer.repurpose_content(content, target_platforms, strategy)
|
||||
|
||||
def create_content_series(
|
||||
self,
|
||||
content: ContentItem,
|
||||
platforms: List[Platform],
|
||||
series_type: str = 'progressive_disclosure'
|
||||
) -> Dict[str, List[ContentItem]]:
|
||||
"""Create a cross-platform content series."""
|
||||
return self.series_repurposer.create_cross_platform_series(
|
||||
content, platforms, series_type
|
||||
)
|
||||
|
||||
def analyze_content_atoms(self, content: str, title: str = "") -> Dict[str, List[str]]:
|
||||
"""Analyze content and extract reusable atoms."""
|
||||
return self.atomizer.atomize_content(content, title)
|
||||
|
||||
def get_repurposing_suggestions(
|
||||
self,
|
||||
content: ContentItem,
|
||||
available_platforms: List[Platform]
|
||||
) -> Dict[str, Any]:
|
||||
"""Get AI-powered suggestions for content repurposing."""
|
||||
try:
|
||||
# Analyze content to suggest best repurposing strategies
|
||||
content_text = content.description or content.notes or ""
|
||||
atoms = self.atomizer.atomize_content(content_text, content.title)
|
||||
|
||||
suggestions = {
|
||||
'recommended_platforms': [],
|
||||
'repurposing_strategies': [],
|
||||
'content_atoms': atoms,
|
||||
'estimated_pieces': 0
|
||||
}
|
||||
|
||||
# Analyze content type and suggest platforms
|
||||
if content.content_type == ContentType.BLOG_POST:
|
||||
suggestions['recommended_platforms'] = [
|
||||
Platform.TWITTER, Platform.LINKEDIN, Platform.INSTAGRAM
|
||||
]
|
||||
suggestions['estimated_pieces'] = len(available_platforms) * 2
|
||||
elif content.content_type == ContentType.VIDEO:
|
||||
suggestions['recommended_platforms'] = [
|
||||
Platform.TWITTER, Platform.INSTAGRAM, Platform.FACEBOOK
|
||||
]
|
||||
suggestions['estimated_pieces'] = len(available_platforms) * 3
|
||||
|
||||
# Suggest strategies based on content richness
|
||||
if len(atoms.get('statistics', [])) > 3:
|
||||
suggestions['repurposing_strategies'].append('data_driven')
|
||||
if len(atoms.get('tips', [])) > 5:
|
||||
suggestions['repurposing_strategies'].append('tip_series')
|
||||
if len(atoms.get('examples', [])) > 2:
|
||||
suggestions['repurposing_strategies'].append('case_study_series')
|
||||
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting repurposing suggestions: {str(e)}")
|
||||
return {
|
||||
'recommended_platforms': [],
|
||||
'repurposing_strategies': [],
|
||||
'content_atoms': {},
|
||||
'estimated_pieces': 0
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
"""
|
||||
Gap analyzer integration for content calendar.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List, Optional
|
||||
from loguru import logger
|
||||
from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Configure logger for content calendar debugging
|
||||
logger.remove() # Remove default handler
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="DEBUG",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan> | <yellow>{function}</yellow> | {message}",
|
||||
filter=lambda record: "content_calendar" in record["name"].lower()
|
||||
)
|
||||
|
||||
class GapAnalyzerIntegration:
|
||||
"""Integrates content gap analysis with content calendar."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the gap analyzer integration."""
|
||||
self.gap_analyzer = ContentGapAnalysis()
|
||||
logger.debug("GapAnalyzerIntegration initialized for content calendar")
|
||||
|
||||
def analyze_gaps(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze content gaps.
|
||||
|
||||
Args:
|
||||
data: Dictionary containing content data
|
||||
|
||||
Returns:
|
||||
Dictionary containing gap analysis results
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Starting gap analysis with data: {json.dumps(data, indent=2)}")
|
||||
# Run gap analysis
|
||||
results = self.gap_analyzer.analyze(data)
|
||||
logger.debug(f"Gap analysis completed with results: {json.dumps(results, indent=2)}")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error analyzing content gaps: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'gaps': [],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
def get_topic_suggestions(
|
||||
self,
|
||||
gap_analysis: Dict[str, Any],
|
||||
platform: str,
|
||||
count: int = 5
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get topic suggestions for a specific platform based on gap analysis.
|
||||
|
||||
Args:
|
||||
gap_analysis: Results from gap analysis
|
||||
platform: Target platform for content
|
||||
count: Number of suggestions to generate
|
||||
|
||||
Returns:
|
||||
List of topic suggestions
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Generating topic suggestions for platform: {platform}, count: {count}")
|
||||
suggestions = []
|
||||
|
||||
for gap in gap_analysis.get('processed_gaps', []):
|
||||
# Generate platform-specific topics
|
||||
platform_topics = self.ai_processor.generate_platform_topics(
|
||||
gap=gap,
|
||||
platform=platform,
|
||||
count=count
|
||||
)
|
||||
logger.debug(f"Generated topics for gap: {json.dumps(platform_topics, indent=2)}")
|
||||
suggestions.extend(platform_topics)
|
||||
|
||||
logger.debug(f"Total suggestions generated: {len(suggestions)}")
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating topic suggestions: {str(e)}")
|
||||
return []
|
||||
|
||||
def analyze_topic_relevance(
|
||||
self,
|
||||
topic: Dict[str, Any],
|
||||
gap_analysis: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze how well a topic addresses content gaps.
|
||||
|
||||
Args:
|
||||
topic: Topic to analyze
|
||||
gap_analysis: Results from gap analysis
|
||||
|
||||
Returns:
|
||||
Dictionary containing relevance analysis
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Analyzing topic relevance: {json.dumps(topic, indent=2)}")
|
||||
relevance = self.ai_processor.analyze_topic_relevance(
|
||||
topic=topic,
|
||||
gaps=gap_analysis.get('gaps', [])
|
||||
)
|
||||
|
||||
logger.debug(f"Topic relevance analysis completed: {json.dumps(relevance, indent=2)}")
|
||||
return relevance
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error analyzing topic relevance: {str(e)}")
|
||||
return {
|
||||
'error': str(e),
|
||||
'score': 0
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from ..core.calendar_manager import CalendarManager
|
||||
from ..core.content_brief import ContentBriefGenerator
|
||||
from .platform_adapters import UnifiedPlatformAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class IntegrationManager:
|
||||
"""Manages integration between content calendar and platform adapters."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the integration manager."""
|
||||
self.calendar_manager = CalendarManager()
|
||||
self.content_brief_generator = ContentBriefGenerator()
|
||||
self.platform_adapter = UnifiedPlatformAdapter()
|
||||
|
||||
def create_cross_platform_calendar(
|
||||
self,
|
||||
start_date: datetime,
|
||||
end_date: datetime,
|
||||
platforms: List[str],
|
||||
content_types: List[str],
|
||||
target_audience: Optional[Dict[str, Any]] = None,
|
||||
industry: Optional[str] = None,
|
||||
keywords: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a cross-platform content calendar."""
|
||||
try:
|
||||
# Generate base calendar
|
||||
calendar = self.calendar_manager.create_calendar(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
content_types=content_types,
|
||||
target_audience=target_audience,
|
||||
industry=industry,
|
||||
keywords=keywords
|
||||
)
|
||||
|
||||
# Adapt content for each platform
|
||||
platform_calendars = {}
|
||||
for platform in platforms:
|
||||
platform_calendars[platform] = self._adapt_calendar_for_platform(
|
||||
calendar=calendar,
|
||||
platform=platform
|
||||
)
|
||||
|
||||
return {
|
||||
'base_calendar': calendar,
|
||||
'platform_calendars': platform_calendars,
|
||||
'metadata': {
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'platforms': platforms,
|
||||
'content_types': content_types,
|
||||
'industry': industry,
|
||||
'keywords': keywords
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating cross-platform calendar: {str(e)}")
|
||||
raise
|
||||
|
||||
def _adapt_calendar_for_platform(
|
||||
self,
|
||||
calendar: Dict[str, Any],
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Adapt calendar content for a specific platform."""
|
||||
try:
|
||||
adapted_calendar = {
|
||||
'platform': platform,
|
||||
'content_items': [],
|
||||
'metadata': calendar.get('metadata', {})
|
||||
}
|
||||
|
||||
# Adapt each content item
|
||||
for item in calendar.get('content_items', []):
|
||||
adapted_item = self._adapt_content_item(item, platform)
|
||||
if adapted_item:
|
||||
adapted_calendar['content_items'].append(adapted_item)
|
||||
|
||||
return adapted_calendar
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adapting calendar for platform {platform}: {str(e)}")
|
||||
return {
|
||||
'platform': platform,
|
||||
'content_items': [],
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _adapt_content_item(
|
||||
self,
|
||||
item: Dict[str, Any],
|
||||
platform: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Adapt a content item for a specific platform."""
|
||||
try:
|
||||
# Generate content brief if not exists
|
||||
if 'brief' not in item:
|
||||
item['brief'] = self.content_brief_generator.generate_brief(item)
|
||||
|
||||
# Adapt content for platform
|
||||
adapted_content = self.platform_adapter.adapt_content(
|
||||
content=item,
|
||||
platform=platform
|
||||
)
|
||||
|
||||
if adapted_content:
|
||||
return {
|
||||
'original_item': item,
|
||||
'adapted_content': adapted_content,
|
||||
'platform_specifics': self.platform_adapter.get_platform_specs(platform)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adapting content item for platform {platform}: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_platform_suggestions(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
platforms: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Get platform-specific suggestions for content."""
|
||||
try:
|
||||
suggestions = {}
|
||||
|
||||
for platform in platforms:
|
||||
platform_suggestions = self.platform_adapter.get_platform_suggestions(
|
||||
content=content,
|
||||
platform=platform
|
||||
)
|
||||
if platform_suggestions:
|
||||
suggestions[platform] = platform_suggestions
|
||||
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting platform suggestions: {str(e)}")
|
||||
return {}
|
||||
|
||||
def validate_platform_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate content for a specific platform."""
|
||||
try:
|
||||
validation_result = self.platform_adapter.validate_content(
|
||||
content=content,
|
||||
platform=platform
|
||||
)
|
||||
|
||||
return {
|
||||
'platform': platform,
|
||||
'is_valid': validation_result,
|
||||
'specifications': self.platform_adapter.get_platform_specs(platform)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating platform content: {str(e)}")
|
||||
return {
|
||||
'platform': platform,
|
||||
'is_valid': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def optimize_cross_platform_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
platforms: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Optimize content for multiple platforms."""
|
||||
try:
|
||||
optimized_content = {}
|
||||
|
||||
for platform in platforms:
|
||||
platform_optimized = self.platform_adapter.optimize_content(
|
||||
content=content,
|
||||
platform=platform
|
||||
)
|
||||
if platform_optimized:
|
||||
optimized_content[platform] = platform_optimized
|
||||
|
||||
return optimized_content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing cross-platform content: {str(e)}")
|
||||
return {}
|
||||
@@ -1,307 +0,0 @@
|
||||
"""
|
||||
Unified platform adapter for content adaptation across different platforms.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, TypedDict
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
|
||||
class ContentItem(TypedDict):
|
||||
"""Type definition for content items."""
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
platforms: List[str]
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
published_at: Optional[datetime]
|
||||
metadata: Dict[str, Any]
|
||||
analytics: Optional[Dict[str, Any]]
|
||||
|
||||
class UnifiedPlatformAdapter:
|
||||
"""Unified adapter for different social media platforms."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the platform adapter."""
|
||||
self.platform_handlers = {
|
||||
'instagram': self._handle_instagram,
|
||||
'linkedin': self._handle_linkedin,
|
||||
'twitter': self._handle_twitter,
|
||||
'facebook': self._handle_facebook
|
||||
}
|
||||
logger.info("UnifiedPlatformAdapter initialized")
|
||||
|
||||
def generate_content(self, platform: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content for a specific platform.
|
||||
|
||||
Args:
|
||||
platform: Target platform
|
||||
data: Content data
|
||||
|
||||
Returns:
|
||||
Dictionary containing generated content
|
||||
"""
|
||||
try:
|
||||
handler = self.platform_handlers.get(platform.lower())
|
||||
if not handler:
|
||||
raise ValueError(f"Unsupported platform: {platform}")
|
||||
|
||||
return handler(data)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error generating content for {platform}: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'content': None
|
||||
}
|
||||
|
||||
def get_content_performance(self, content_item: ContentItem) -> Dict[str, Any]:
|
||||
"""Get performance metrics for content across platforms."""
|
||||
try:
|
||||
logger.info(f"Getting performance metrics for content: {getattr(content_item, 'title', 'Untitled')}")
|
||||
|
||||
# Get platform from content item
|
||||
platforms = getattr(content_item, 'platforms', None)
|
||||
if platforms and len(platforms) > 0:
|
||||
platform = platforms[0].name if hasattr(platforms[0], 'name') else str(platforms[0])
|
||||
else:
|
||||
platform = 'Unknown'
|
||||
|
||||
# Initialize performance metrics
|
||||
performance = {
|
||||
'engagement_metrics': {
|
||||
'likes': 0,
|
||||
'comments': 0,
|
||||
'shares': 0,
|
||||
'reach': 0
|
||||
},
|
||||
'seo_metrics': {
|
||||
'impressions': 0,
|
||||
'clicks': 0,
|
||||
'ctr': 0,
|
||||
'position': 0
|
||||
},
|
||||
'conversion_metrics': {
|
||||
'conversions': 0,
|
||||
'conversion_rate': 0,
|
||||
'revenue': 0
|
||||
},
|
||||
'platform_specific': {},
|
||||
'performance_trends': [],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
# Add platform-specific metrics
|
||||
if platform.upper() == 'WEBSITE':
|
||||
performance['platform_specific'] = {
|
||||
'bounce_rate': 0,
|
||||
'time_on_page': 0,
|
||||
'page_views': 0
|
||||
}
|
||||
|
||||
return performance
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error getting content performance: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'metrics': {},
|
||||
'trends': {},
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
def _handle_instagram(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Instagram content generation."""
|
||||
try:
|
||||
# Generate Instagram-specific content
|
||||
caption = metadesc_generator_main(data)
|
||||
hashtags = self._generate_hashtags(data)
|
||||
|
||||
return {
|
||||
'platform': 'instagram',
|
||||
'content': {
|
||||
'caption': caption,
|
||||
'hashtags': hashtags,
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Instagram content: {str(e)}")
|
||||
return {
|
||||
'platform': 'instagram',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _handle_linkedin(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle LinkedIn content generation."""
|
||||
try:
|
||||
# Generate LinkedIn-specific content
|
||||
post = metadesc_generator_main(data)
|
||||
|
||||
return {
|
||||
'platform': 'linkedin',
|
||||
'content': {
|
||||
'post': post,
|
||||
'engagement_optimization': self._get_engagement_suggestions(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn content: {str(e)}")
|
||||
return {
|
||||
'platform': 'linkedin',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _handle_twitter(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Twitter content generation."""
|
||||
try:
|
||||
# Generate Twitter-specific content
|
||||
tweet = metadesc_generator_main(data)
|
||||
hashtags = self._generate_hashtags(data)
|
||||
|
||||
return {
|
||||
'platform': 'twitter',
|
||||
'content': {
|
||||
'tweet': tweet,
|
||||
'hashtags': hashtags,
|
||||
'thread_structure': self._get_thread_structure(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Twitter content: {str(e)}")
|
||||
return {
|
||||
'platform': 'twitter',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _handle_facebook(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Facebook content generation."""
|
||||
try:
|
||||
# Generate Facebook-specific content
|
||||
post = metadesc_generator_main(data)
|
||||
|
||||
return {
|
||||
'platform': 'facebook',
|
||||
'content': {
|
||||
'post': post,
|
||||
'engagement_optimization': self._get_engagement_suggestions(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Facebook content: {str(e)}")
|
||||
return {
|
||||
'platform': 'facebook',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _generate_hashtags(self, data: Dict[str, Any]) -> List[str]:
|
||||
"""Generate relevant hashtags for content."""
|
||||
try:
|
||||
# Extract keywords from content
|
||||
keywords = data.get('keywords', [])
|
||||
|
||||
# Add platform-specific hashtags
|
||||
platform = data.get('platform', '').lower()
|
||||
platform_hashtags = {
|
||||
'instagram': ['#instagood', '#photooftheday'],
|
||||
'twitter': ['#trending', '#followme'],
|
||||
'linkedin': ['#business', '#professional'],
|
||||
'facebook': ['#social', '#community']
|
||||
}.get(platform, [])
|
||||
|
||||
return keywords + platform_hashtags
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating hashtags: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_media_suggestions(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Get media suggestions for content."""
|
||||
try:
|
||||
# Generate media suggestions based on content type
|
||||
content_type = data.get('type', 'post')
|
||||
|
||||
suggestions = []
|
||||
if content_type == 'blog':
|
||||
suggestions.append({
|
||||
'type': 'featured_image',
|
||||
'description': 'Main blog post image',
|
||||
'dimensions': '1200x630'
|
||||
})
|
||||
elif content_type == 'social':
|
||||
suggestions.append({
|
||||
'type': 'post_image',
|
||||
'description': 'Social media post image',
|
||||
'dimensions': '1080x1080'
|
||||
})
|
||||
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting media suggestions: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_engagement_suggestions(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get engagement optimization suggestions."""
|
||||
try:
|
||||
return {
|
||||
'best_posting_times': ['9:00 AM', '5:00 PM'],
|
||||
'engagement_tips': [
|
||||
'Ask questions to encourage comments',
|
||||
'Use relevant hashtags',
|
||||
'Include a clear call-to-action'
|
||||
],
|
||||
'content_length': {
|
||||
'optimal': '150-200 characters',
|
||||
'maximum': '300 characters'
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting engagement suggestions: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _get_thread_structure(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Get thread structure for Twitter threads."""
|
||||
try:
|
||||
content = data.get('content', '')
|
||||
sentences = content.split('.')
|
||||
|
||||
thread = []
|
||||
current_tweet = ''
|
||||
|
||||
for sentence in sentences:
|
||||
if len(current_tweet + sentence) <= 280:
|
||||
current_tweet += sentence + '.'
|
||||
else:
|
||||
if current_tweet:
|
||||
thread.append({
|
||||
'content': current_tweet.strip(),
|
||||
'type': 'tweet'
|
||||
})
|
||||
current_tweet = sentence + '.'
|
||||
|
||||
if current_tweet:
|
||||
thread.append({
|
||||
'content': current_tweet.strip(),
|
||||
'type': 'tweet'
|
||||
})
|
||||
|
||||
return thread
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating thread structure: {str(e)}")
|
||||
return []
|
||||
@@ -1,219 +0,0 @@
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from ...meta_desc_generator import generate_blog_metadesc
|
||||
from ...content_title_generator import generate_blog_titles
|
||||
from ...seo_structured_data import generate_json_data
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SEOOptimizer:
|
||||
"""Integrates SEO tools with content calendar system."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the SEO optimizer."""
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Configure logging for SEO optimizer."""
|
||||
logger.setLevel(logging.INFO)
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
def optimize_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
content_type: str = 'article',
|
||||
language: str = 'English',
|
||||
search_intent: str = 'Informational Intent'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize content for SEO using existing tools.
|
||||
|
||||
Args:
|
||||
content: Content to optimize
|
||||
content_type: Type of content (article, product, etc.)
|
||||
language: Content language
|
||||
search_intent: Search intent type
|
||||
|
||||
Returns:
|
||||
Optimized content with SEO elements
|
||||
"""
|
||||
try:
|
||||
# Extract content details
|
||||
title = content.get('title', '')
|
||||
keywords = content.get('keywords', [])
|
||||
content_text = content.get('content', '')
|
||||
|
||||
# Generate SEO elements
|
||||
optimized_title = self._optimize_title(
|
||||
title=title,
|
||||
keywords=keywords,
|
||||
content_type=content_type,
|
||||
language=language,
|
||||
search_intent=search_intent
|
||||
)
|
||||
|
||||
meta_description = self._generate_meta_description(
|
||||
keywords=keywords,
|
||||
content_type=content_type,
|
||||
language=language,
|
||||
search_intent=search_intent
|
||||
)
|
||||
|
||||
structured_data = self._generate_structured_data(
|
||||
content=content,
|
||||
content_type=content_type
|
||||
)
|
||||
|
||||
return {
|
||||
'original_content': content,
|
||||
'seo_optimized': {
|
||||
'title': optimized_title,
|
||||
'meta_description': meta_description,
|
||||
'structured_data': structured_data,
|
||||
'keywords': keywords,
|
||||
'content_type': content_type,
|
||||
'language': language,
|
||||
'search_intent': search_intent
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing content: {str(e)}")
|
||||
return {
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _optimize_title(
|
||||
self,
|
||||
title: str,
|
||||
keywords: List[str],
|
||||
content_type: str,
|
||||
language: str,
|
||||
search_intent: str
|
||||
) -> List[str]:
|
||||
"""Generate SEO-optimized titles."""
|
||||
try:
|
||||
# Convert keywords list to comma-separated string
|
||||
keywords_str = ', '.join(keywords)
|
||||
|
||||
# Generate titles using existing tool
|
||||
titles = generate_blog_titles(
|
||||
input_blog_keywords=keywords_str,
|
||||
input_blog_content=title,
|
||||
input_title_type=content_type,
|
||||
input_title_intent=search_intent,
|
||||
input_language=language
|
||||
)
|
||||
|
||||
return titles.split('\n') if titles else []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing title: {str(e)}")
|
||||
return []
|
||||
|
||||
def _generate_meta_description(
|
||||
self,
|
||||
keywords: List[str],
|
||||
content_type: str,
|
||||
language: str,
|
||||
search_intent: str
|
||||
) -> List[str]:
|
||||
"""Generate SEO-optimized meta descriptions."""
|
||||
try:
|
||||
# Convert keywords list to comma-separated string
|
||||
keywords_str = ', '.join(keywords)
|
||||
|
||||
# Generate meta descriptions using existing tool
|
||||
descriptions = generate_blog_metadesc(
|
||||
keywords=keywords_str,
|
||||
tone='Informative',
|
||||
search_type=search_intent,
|
||||
language=language
|
||||
)
|
||||
|
||||
return descriptions.split('\n') if descriptions else []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating meta description: {str(e)}")
|
||||
return []
|
||||
|
||||
def _generate_structured_data(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
content_type: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Generate structured data for content."""
|
||||
try:
|
||||
# Prepare content details for structured data
|
||||
details = {
|
||||
'Headline': content.get('title', ''),
|
||||
'Author': content.get('author', ''),
|
||||
'Date Published': content.get('publish_date', datetime.now().isoformat()),
|
||||
'Keywords': ', '.join(content.get('keywords', [])),
|
||||
'Description': content.get('description', ''),
|
||||
'Image URL': content.get('image_url', '')
|
||||
}
|
||||
|
||||
# Generate structured data using existing tool
|
||||
structured_data = generate_json_data(
|
||||
content_type=content_type,
|
||||
details=details,
|
||||
url=content.get('url', '')
|
||||
)
|
||||
|
||||
return structured_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating structured data: {str(e)}")
|
||||
return None
|
||||
|
||||
def optimize_calendar_content(
|
||||
self,
|
||||
calendar: Dict[str, Any],
|
||||
content_type: str = 'article',
|
||||
language: str = 'English',
|
||||
search_intent: str = 'Informational Intent'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize all content in calendar for SEO.
|
||||
|
||||
Args:
|
||||
calendar: Content calendar to optimize
|
||||
content_type: Type of content
|
||||
language: Content language
|
||||
search_intent: Search intent type
|
||||
|
||||
Returns:
|
||||
Calendar with SEO-optimized content
|
||||
"""
|
||||
try:
|
||||
optimized_calendar = {
|
||||
'metadata': calendar.get('metadata', {}),
|
||||
'content_items': []
|
||||
}
|
||||
|
||||
# Optimize each content item
|
||||
for item in calendar.get('content_items', []):
|
||||
optimized_item = self.optimize_content(
|
||||
content=item,
|
||||
content_type=content_type,
|
||||
language=language,
|
||||
search_intent=search_intent
|
||||
)
|
||||
if optimized_item:
|
||||
optimized_calendar['content_items'].append(optimized_item)
|
||||
|
||||
return optimized_calendar
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing calendar content: {str(e)}")
|
||||
return {
|
||||
'error': str(e)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
"""SEO tools integration for content calendar."""
|
||||
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
from typing import Dict, Any, List, Optional
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
# Configure logger
|
||||
logger.remove() # Remove default handler
|
||||
logger.add(
|
||||
"logs/seo_tools_integration.log",
|
||||
rotation="50 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
|
||||
)
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="INFO",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||
)
|
||||
|
||||
# Ensure logs directory exists
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
|
||||
class SEOToolsIntegration:
|
||||
"""Integration with SEO tools for content calendar."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the SEO tools integration."""
|
||||
self.website_analyzer = WebsiteAnalyzer()
|
||||
logger.info("SEOToolsIntegration initialized")
|
||||
|
||||
def analyze_content(self, url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze content for SEO optimization.
|
||||
|
||||
Args:
|
||||
url: The URL to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary containing SEO analysis results
|
||||
"""
|
||||
try:
|
||||
# Analyze website
|
||||
analysis = self.website_analyzer.analyze_website(url)
|
||||
if not analysis.get('success', False):
|
||||
return {
|
||||
'error': analysis.get('error', 'Unknown error in analysis'),
|
||||
'seo_score': 0,
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
# Extract SEO information
|
||||
seo_info = analysis['data']['analysis']['seo_info']
|
||||
|
||||
return {
|
||||
'seo_score': seo_info.get('overall_score', 0),
|
||||
'meta_tags': seo_info.get('meta_tags', {}),
|
||||
'content': seo_info.get('content', {}),
|
||||
'recommendations': seo_info.get('recommendations', [])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error analyzing content: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'seo_score': 0,
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
def generate_title(self, url: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate SEO-optimized title.
|
||||
|
||||
Args:
|
||||
url: The URL to analyze
|
||||
|
||||
Returns:
|
||||
Dictionary containing title suggestions
|
||||
"""
|
||||
return ai_title_generator(url)
|
||||
|
||||
def optimize_content(self, content: str, keywords: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize content for SEO.
|
||||
|
||||
Args:
|
||||
content: The content to optimize
|
||||
keywords: List of target keywords
|
||||
|
||||
Returns:
|
||||
Dictionary containing optimization suggestions
|
||||
"""
|
||||
try:
|
||||
# Prepare prompt for content optimization
|
||||
prompt = f"""Optimize the following content for SEO:
|
||||
|
||||
Content: {content}
|
||||
Target Keywords: {', '.join(keywords)}
|
||||
|
||||
Provide optimization suggestions for:
|
||||
1. Keyword usage and placement
|
||||
2. Content structure and readability
|
||||
3. Meta information
|
||||
4. Internal linking opportunities
|
||||
5. Content length and depth
|
||||
|
||||
Format the response as JSON with 'suggestions' and 'score' keys."""
|
||||
|
||||
# Get AI optimization suggestions
|
||||
suggestions = llm_text_gen(
|
||||
prompt=prompt,
|
||||
system_prompt="You are an SEO expert specializing in content optimization.",
|
||||
response_format="json_object"
|
||||
)
|
||||
|
||||
if not suggestions:
|
||||
return {
|
||||
'error': 'Failed to generate optimization suggestions',
|
||||
'suggestions': [],
|
||||
'score': 0
|
||||
}
|
||||
|
||||
return {
|
||||
'suggestions': suggestions.get('suggestions', []),
|
||||
'score': suggestions.get('score', 0)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error optimizing content: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'suggestions': [],
|
||||
'score': 0
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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
|
||||
@@ -1,137 +0,0 @@
|
||||
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}")
|
||||
@@ -1,51 +0,0 @@
|
||||
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.")
|
||||
@@ -1,294 +0,0 @@
|
||||
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}")
|
||||
@@ -1,2 +0,0 @@
|
||||
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>"
|
||||
@@ -1,22 +0,0 @@
|
||||
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)
|
||||
@@ -1,498 +0,0 @@
|
||||
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
|
||||
@@ -1,517 +0,0 @@
|
||||
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()
|
||||
@@ -1,457 +0,0 @@
|
||||
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.")
|
||||
@@ -1,81 +0,0 @@
|
||||
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)}")
|
||||
@@ -1,638 +0,0 @@
|
||||
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()
|
||||
@@ -1,30 +0,0 @@
|
||||
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
|
||||
@@ -1,198 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any
|
||||
import calendar
|
||||
import random
|
||||
|
||||
def calculate_publish_dates(
|
||||
topics: List[Dict[str, Any]],
|
||||
start_date: datetime,
|
||||
duration: str
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Calculate optimal publish dates for content topics.
|
||||
|
||||
Args:
|
||||
topics: List of content topics to schedule
|
||||
start_date: When to start publishing
|
||||
duration: How long the calendar should span ('weekly', 'monthly', 'quarterly')
|
||||
|
||||
Returns:
|
||||
Dictionary mapping dates to scheduled content
|
||||
"""
|
||||
# Calculate end date based on duration
|
||||
end_date = _calculate_end_date(start_date, duration)
|
||||
|
||||
# Get all dates in range
|
||||
dates = _get_dates_in_range(start_date, end_date)
|
||||
|
||||
# Calculate optimal posting frequency
|
||||
frequency = _calculate_posting_frequency(len(topics), len(dates))
|
||||
|
||||
# Schedule content
|
||||
schedule = _schedule_content(topics, dates, frequency)
|
||||
|
||||
return schedule
|
||||
|
||||
def _calculate_end_date(start_date: datetime, duration: str) -> datetime:
|
||||
"""Calculate end date based on duration."""
|
||||
if duration == 'weekly':
|
||||
return start_date + timedelta(days=7)
|
||||
elif duration == 'monthly':
|
||||
# Add one month
|
||||
if start_date.month == 12:
|
||||
return datetime(start_date.year + 1, 1, start_date.day)
|
||||
return datetime(start_date.year, start_date.month + 1, start_date.day)
|
||||
elif duration == 'quarterly':
|
||||
# Add three months
|
||||
new_month = start_date.month + 3
|
||||
new_year = start_date.year
|
||||
if new_month > 12:
|
||||
new_month -= 12
|
||||
new_year += 1
|
||||
return datetime(new_year, new_month, start_date.day)
|
||||
else:
|
||||
raise ValueError(f"Invalid duration: {duration}")
|
||||
|
||||
def _get_dates_in_range(
|
||||
start_date: datetime,
|
||||
end_date: datetime
|
||||
) -> List[datetime]:
|
||||
"""Get all dates in the given range."""
|
||||
dates = []
|
||||
current_date = start_date
|
||||
|
||||
while current_date <= end_date:
|
||||
# Skip weekends
|
||||
if current_date.weekday() < 5: # 0-4 are weekdays
|
||||
dates.append(current_date)
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
return dates
|
||||
|
||||
def _calculate_posting_frequency(
|
||||
num_topics: int,
|
||||
num_dates: int
|
||||
) -> Dict[str, int]:
|
||||
"""
|
||||
Calculate optimal posting frequency based on number of topics and dates.
|
||||
|
||||
Returns:
|
||||
Dictionary with posting frequency for each content type
|
||||
"""
|
||||
# Calculate base frequency
|
||||
base_frequency = num_dates / num_topics
|
||||
|
||||
# Adjust for content types
|
||||
return {
|
||||
'blog_post': max(1, int(base_frequency * 0.4)), # 40% of content
|
||||
'social_media': max(1, int(base_frequency * 0.3)), # 30% of content
|
||||
'video': max(1, int(base_frequency * 0.2)), # 20% of content
|
||||
'newsletter': max(1, int(base_frequency * 0.1)) # 10% of content
|
||||
}
|
||||
|
||||
def _schedule_content(
|
||||
topics: List[Dict[str, Any]],
|
||||
dates: List[datetime],
|
||||
frequency: Dict[str, int]
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Schedule content topics across available dates.
|
||||
|
||||
Args:
|
||||
topics: List of content topics to schedule
|
||||
dates: Available dates for scheduling
|
||||
frequency: Posting frequency for each content type
|
||||
|
||||
Returns:
|
||||
Dictionary mapping dates to scheduled content
|
||||
"""
|
||||
schedule = {}
|
||||
current_date_index = 0
|
||||
|
||||
# Group topics by content type
|
||||
topics_by_type = _group_topics_by_type(topics)
|
||||
|
||||
# Schedule each content type
|
||||
for content_type, type_topics in topics_by_type.items():
|
||||
type_frequency = frequency.get(content_type, 1)
|
||||
|
||||
for topic in type_topics:
|
||||
# Find next available date
|
||||
while current_date_index < len(dates):
|
||||
date = dates[current_date_index]
|
||||
date_str = date.strftime('%Y-%m-%d')
|
||||
|
||||
# Check if date is available
|
||||
if date_str not in schedule:
|
||||
schedule[date_str] = []
|
||||
|
||||
# Add topic to schedule
|
||||
schedule[date_str].append(topic)
|
||||
|
||||
# Move to next date based on frequency
|
||||
current_date_index += type_frequency
|
||||
break
|
||||
|
||||
# If we've used all dates, wrap around
|
||||
if current_date_index >= len(dates):
|
||||
current_date_index = 0
|
||||
|
||||
return schedule
|
||||
|
||||
def _group_topics_by_type(
|
||||
topics: List[Dict[str, Any]]
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Group topics by their content type."""
|
||||
grouped = {}
|
||||
|
||||
for topic in topics:
|
||||
content_type = topic.get('content_type', 'blog_post')
|
||||
if content_type not in grouped:
|
||||
grouped[content_type] = []
|
||||
grouped[content_type].append(topic)
|
||||
|
||||
return grouped
|
||||
|
||||
def get_optimal_posting_time(
|
||||
content_type: str,
|
||||
platform: str
|
||||
) -> datetime.time:
|
||||
"""
|
||||
Get optimal posting time for content type and platform.
|
||||
|
||||
Args:
|
||||
content_type: Type of content
|
||||
platform: Target platform
|
||||
|
||||
Returns:
|
||||
Optimal time to post
|
||||
"""
|
||||
# Default optimal times (can be customized based on platform analytics)
|
||||
optimal_times = {
|
||||
'blog_post': {
|
||||
'website': datetime.time(9, 0), # 9 AM
|
||||
'medium': datetime.time(10, 0) # 10 AM
|
||||
},
|
||||
'social_media': {
|
||||
'facebook': datetime.time(15, 0), # 3 PM
|
||||
'twitter': datetime.time(12, 0), # 12 PM
|
||||
'linkedin': datetime.time(8, 0), # 8 AM
|
||||
'instagram': datetime.time(19, 0) # 7 PM
|
||||
},
|
||||
'video': {
|
||||
'youtube': datetime.time(14, 0) # 2 PM
|
||||
},
|
||||
'newsletter': {
|
||||
'email': datetime.time(6, 0) # 6 AM
|
||||
}
|
||||
}
|
||||
|
||||
# Get optimal time for content type and platform
|
||||
content_times = optimal_times.get(content_type, {})
|
||||
optimal_time = content_times.get(platform)
|
||||
|
||||
if optimal_time is None:
|
||||
# Default to 9 AM if no specific time is set
|
||||
optimal_time = datetime.time(9, 0)
|
||||
|
||||
return optimal_time
|
||||
@@ -1,154 +0,0 @@
|
||||
import functools
|
||||
import logging
|
||||
from typing import Any, Callable, TypeVar, cast
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def handle_calendar_error(func: Callable[..., T]) -> Callable[..., T]:
|
||||
"""
|
||||
Decorator to handle errors in calendar operations.
|
||||
|
||||
Args:
|
||||
func: Function to decorate
|
||||
|
||||
Returns:
|
||||
Decorated function with error handling
|
||||
"""
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> T:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid input in {func.__name__}: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {func.__name__}: {str(e)}")
|
||||
raise CalendarError(f"Calendar operation failed: {str(e)}")
|
||||
return cast(Callable[..., T], wrapper)
|
||||
|
||||
class CalendarError(Exception):
|
||||
"""Base exception for calendar-related errors."""
|
||||
pass
|
||||
|
||||
class ContentError(CalendarError):
|
||||
"""Exception for content-related errors."""
|
||||
pass
|
||||
|
||||
class SchedulingError(CalendarError):
|
||||
"""Exception for scheduling-related errors."""
|
||||
pass
|
||||
|
||||
class ValidationError(CalendarError):
|
||||
"""Exception for validation-related errors."""
|
||||
pass
|
||||
|
||||
def validate_date_range(
|
||||
start_date: datetime,
|
||||
end_date: datetime
|
||||
) -> None:
|
||||
"""
|
||||
Validate date range for calendar operations.
|
||||
|
||||
Args:
|
||||
start_date: Start date
|
||||
end_date: End date
|
||||
|
||||
Raises:
|
||||
ValidationError: If date range is invalid
|
||||
"""
|
||||
if not isinstance(start_date, datetime):
|
||||
raise ValidationError("Start date must be a datetime object")
|
||||
|
||||
if not isinstance(end_date, datetime):
|
||||
raise ValidationError("End date must be a datetime object")
|
||||
|
||||
if start_date > end_date:
|
||||
raise ValidationError("Start date must be before end date")
|
||||
|
||||
if (end_date - start_date).days > 365:
|
||||
raise ValidationError("Calendar duration cannot exceed one year")
|
||||
|
||||
def validate_content_item(content: dict) -> None:
|
||||
"""
|
||||
Validate content item structure.
|
||||
|
||||
Args:
|
||||
content: Content item to validate
|
||||
|
||||
Raises:
|
||||
ValidationError: If content item is invalid
|
||||
"""
|
||||
required_fields = ['title', 'description', 'content_type', 'platforms']
|
||||
|
||||
for field in required_fields:
|
||||
if field not in content:
|
||||
raise ValidationError(f"Missing required field: {field}")
|
||||
|
||||
if not isinstance(content['platforms'], list):
|
||||
raise ValidationError("Platforms must be a list")
|
||||
|
||||
if not content['platforms']:
|
||||
raise ValidationError("At least one platform must be specified")
|
||||
|
||||
def validate_calendar_duration(duration: str) -> None:
|
||||
"""
|
||||
Validate calendar duration.
|
||||
|
||||
Args:
|
||||
duration: Duration to validate ('weekly', 'monthly', 'quarterly')
|
||||
|
||||
Raises:
|
||||
ValidationError: If duration is invalid
|
||||
"""
|
||||
valid_durations = ['weekly', 'monthly', 'quarterly']
|
||||
|
||||
if duration not in valid_durations:
|
||||
raise ValidationError(
|
||||
f"Invalid duration: {duration}. "
|
||||
f"Must be one of: {', '.join(valid_durations)}"
|
||||
)
|
||||
|
||||
def log_calendar_operation(
|
||||
operation: str,
|
||||
details: dict
|
||||
) -> None:
|
||||
"""
|
||||
Log calendar operation details.
|
||||
|
||||
Args:
|
||||
operation: Name of the operation
|
||||
details: Operation details to log
|
||||
"""
|
||||
logger.info(f"Calendar operation: {operation}")
|
||||
logger.debug(f"Operation details: {details}")
|
||||
|
||||
def handle_api_error(
|
||||
error: Exception,
|
||||
operation: str
|
||||
) -> None:
|
||||
"""
|
||||
Handle API-related errors.
|
||||
|
||||
Args:
|
||||
error: The error that occurred
|
||||
operation: The operation that failed
|
||||
"""
|
||||
logger.error(f"API error in {operation}: {str(error)}")
|
||||
raise CalendarError(f"API operation failed: {str(error)}")
|
||||
|
||||
def handle_integration_error(
|
||||
error: Exception,
|
||||
integration: str
|
||||
) -> None:
|
||||
"""
|
||||
Handle integration-related errors.
|
||||
|
||||
Args:
|
||||
error: The error that occurred
|
||||
integration: The integration that failed
|
||||
"""
|
||||
logger.error(f"Integration error with {integration}: {str(error)}")
|
||||
raise CalendarError(f"Integration failed: {str(error)}")
|
||||
@@ -1,344 +0,0 @@
|
||||
# 🎯 AI Content Performance Predictor
|
||||
|
||||
**LLM-Powered Content Success Prediction for Solo Developers**
|
||||
|
||||
The AI Content Performance Predictor is an intelligent feature that leverages Large Language Models (LLMs) to analyze your content and predict its potential success before you publish. Perfect for solo developers and entrepreneurs who need smart content insights without complex ML infrastructure.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Architecture](#architecture)
|
||||
- [AI Analysis Engine](#ai-analysis-engine)
|
||||
- [API Reference](#api-reference)
|
||||
- [File Structure](#file-structure)
|
||||
- [Configuration](#configuration)
|
||||
- [Development](#development)
|
||||
- [Performance Metrics](#performance-metrics)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## 🔍 Overview
|
||||
|
||||
The AI Content Performance Predictor uses advanced LLM capabilities to provide intelligent content analysis and predictions:
|
||||
|
||||
- **LLM-Powered Analysis**: Uses your existing `llm_text_gen` integration for smart predictions
|
||||
- **Platform-Specific Insights**: Tailored analysis for Twitter, LinkedIn, Facebook, Instagram, and more
|
||||
- **Zero Training Required**: No ML model training needed - works immediately
|
||||
- **Solo Developer Friendly**: Designed for resource-constrained environments
|
||||
- **Real-time Predictions**: Instant analysis and recommendations
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- **🧠 AI-Powered Intelligence**: Leverages LLM understanding for content analysis
|
||||
- **⚡ Instant Predictions**: No waiting for model training or data collection
|
||||
- **📊 Smart Insights**: Platform-specific recommendations and optimization tips
|
||||
- **🎯 Success Scoring**: Comprehensive performance scoring system
|
||||
- **🔄 Adaptive Learning**: Improves recommendations based on platform best practices
|
||||
- **🎨 Multi-platform**: Optimized for different social media platforms
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Core Features
|
||||
|
||||
#### 1. **AI Prediction Engine**
|
||||
- Overall performance score (0-100)
|
||||
- Success probability percentage
|
||||
- Platform-specific optimization
|
||||
- Content quality assessment
|
||||
|
||||
#### 2. **LLM Integration**
|
||||
- Uses existing Alwrity LLM infrastructure
|
||||
- No additional API costs or setup
|
||||
- Intelligent content understanding
|
||||
- Context-aware analysis
|
||||
|
||||
#### 3. **Platform Optimization**
|
||||
- Twitter: Character limits, hashtag optimization, engagement factors
|
||||
- LinkedIn: Professional tone, optimal length, business focus
|
||||
- Facebook: Community engagement, storytelling elements
|
||||
- Instagram: Visual content readiness, hashtag strategy
|
||||
|
||||
#### 4. **Smart Recommendations**
|
||||
- Content improvement suggestions
|
||||
- Optimal posting strategies
|
||||
- Engagement enhancement tips
|
||||
- SEO optimization advice
|
||||
|
||||
#### 5. **Interactive UI**
|
||||
- Clean Streamlit interface
|
||||
- Real-time analysis
|
||||
- Visual performance indicators
|
||||
- Actionable insights display
|
||||
|
||||
### Analysis Categories
|
||||
|
||||
1. **📈 Engagement Potential**: Predicted likes, comments, shares
|
||||
2. **🎯 Content Quality**: Overall content effectiveness score
|
||||
3. **⏰ Timing Insights**: Optimal posting time recommendations
|
||||
4. **🔍 SEO Score**: Search engine optimization assessment
|
||||
5. **🏷️ Hashtag Strategy**: Hashtag effectiveness analysis
|
||||
6. **👥 Audience Alignment**: Content-audience fit assessment
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Already included in Alwrity - no additional installation required!
|
||||
# Uses existing dependencies: streamlit, llm_text_gen
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Auto-Integration** (already included):
|
||||
```python
|
||||
# Available in AI Writer Dashboard
|
||||
# Access via: "AI Content Performance Predictor"
|
||||
```
|
||||
|
||||
2. **Direct Usage**:
|
||||
```python
|
||||
from lib.content_performance_predictor.ai_performance_predictor import AIContentPerformancePredictor
|
||||
```
|
||||
|
||||
3. **UI Component**:
|
||||
```python
|
||||
from lib.content_performance_predictor.ai_performance_predictor import render_ai_predictor_ui
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### Through AI Writer Dashboard
|
||||
|
||||
1. Open Alwrity
|
||||
2. Navigate to "AI Writer Dashboard"
|
||||
3. Select "🎯 AI Content Performance Predictor"
|
||||
4. Enter your content and select platform
|
||||
5. Get instant AI-powered predictions!
|
||||
|
||||
### Direct API Usage
|
||||
|
||||
```python
|
||||
from lib.content_performance_predictor.ai_performance_predictor import AIContentPerformancePredictor
|
||||
|
||||
# Initialize predictor
|
||||
predictor = AIContentPerformancePredictor()
|
||||
|
||||
# Analyze content
|
||||
result = await predictor.predict_performance(
|
||||
content="Your amazing content here!",
|
||||
platform="twitter",
|
||||
target_audience="tech entrepreneurs"
|
||||
)
|
||||
|
||||
print(f"Overall Score: {result['overall_score']}")
|
||||
print(f"Recommendations: {result['recommendations']}")
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```python
|
||||
import streamlit as st
|
||||
from lib.content_performance_predictor.ai_performance_predictor import render_ai_predictor_ui
|
||||
|
||||
# Add to your Streamlit app
|
||||
st.title("Content Analysis")
|
||||
render_ai_predictor_ui()
|
||||
```
|
||||
|
||||
### Batch Content Analysis
|
||||
|
||||
```python
|
||||
# Analyze multiple pieces of content
|
||||
contents = [
|
||||
{"content": "Post 1", "platform": "twitter"},
|
||||
{"content": "Post 2", "platform": "linkedin"},
|
||||
{"content": "Post 3", "platform": "facebook"}
|
||||
]
|
||||
|
||||
for content_data in contents:
|
||||
result = await predictor.predict_performance(**content_data)
|
||||
print(f"Content: {content_data['content'][:50]}...")
|
||||
print(f"Score: {result['overall_score']}")
|
||||
print("---")
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ STREAMLIT UI │
|
||||
│ (render_ai_predictor_ui) │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────┴───────────────────────────────────────────┐
|
||||
│ AI PREDICTION ENGINE │
|
||||
│ (AIContentPerformancePredictor) │
|
||||
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ AI Analysis │ │ Platform Configs │ │
|
||||
│ │ (LLM-powered) │ │ (Twitter, LinkedIn, etc.) │ │
|
||||
│ └─────────────────┘ └─────────────────────────────────┘ │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────┴───────────────────────────────────────────┐
|
||||
│ ALWRITY LLM ENGINE │
|
||||
│ (llm_text_gen) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component Details
|
||||
|
||||
1. **AIContentPerformancePredictor**: Main prediction class
|
||||
2. **Platform Configurations**: Optimized settings for each platform
|
||||
3. **LLM Integration**: Seamless integration with existing AI infrastructure
|
||||
4. **UI Components**: Interactive Streamlit interface
|
||||
|
||||
## 🧠 AI Analysis Engine
|
||||
|
||||
### LLM-Powered Predictions
|
||||
|
||||
The predictor uses sophisticated prompts to analyze:
|
||||
|
||||
- **Content Quality**: Grammar, readability, engagement potential
|
||||
- **Platform Fit**: Alignment with platform best practices
|
||||
- **Audience Appeal**: Target audience relevance
|
||||
- **Optimization Opportunities**: Specific improvement suggestions
|
||||
|
||||
### Platform-Specific Analysis
|
||||
|
||||
#### Twitter Configuration
|
||||
- Optimal Length: 100-280 characters
|
||||
- Hashtags: 1-3 relevant hashtags
|
||||
- Engagement Factors: Questions, calls-to-action, trending topics
|
||||
|
||||
#### LinkedIn Configuration
|
||||
- Optimal Length: 150-300 words
|
||||
- Professional Tone: Business-focused language
|
||||
- Engagement: Industry insights, professional experiences
|
||||
|
||||
#### Facebook Configuration
|
||||
- Optimal Length: 40-80 characters for high engagement
|
||||
- Community Focus: Shareable, relatable content
|
||||
- Visual Ready: Content that complements images/videos
|
||||
|
||||
#### Instagram Configuration
|
||||
- Visual Emphasis: Content supporting visual storytelling
|
||||
- Hashtags: 5-10 strategic hashtags
|
||||
- Story Potential: Content suitable for Instagram Stories
|
||||
|
||||
## 📊 Performance Metrics
|
||||
|
||||
### Success Indicators
|
||||
|
||||
- **Overall Score**: 0-100 performance prediction
|
||||
- **Platform Alignment**: How well content fits the platform
|
||||
- **Engagement Prediction**: Expected interaction levels
|
||||
- **Optimization Score**: Room for improvement rating
|
||||
|
||||
### Recommendation Categories
|
||||
|
||||
1. **Content Improvements**: Direct text enhancements
|
||||
2. **Platform Optimization**: Platform-specific adjustments
|
||||
3. **Timing Suggestions**: Optimal posting strategies
|
||||
4. **Engagement Boosters**: Tactics to increase interaction
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Platform Settings
|
||||
|
||||
Located in `ai_performance_predictor.py`:
|
||||
|
||||
```python
|
||||
PLATFORM_CONFIGS = {
|
||||
"twitter": {
|
||||
"optimal_length": {"min": 100, "max": 280},
|
||||
"hashtag_range": {"min": 1, "max": 3},
|
||||
"engagement_factors": ["questions", "cta", "trending"]
|
||||
},
|
||||
# ... other platforms
|
||||
}
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
You can modify:
|
||||
- Platform-specific parameters
|
||||
- Analysis prompts
|
||||
- Scoring algorithms
|
||||
- UI components
|
||||
|
||||
## 🚀 Development
|
||||
|
||||
### Adding New Platforms
|
||||
|
||||
1. Add platform config to `PLATFORM_CONFIGS`
|
||||
2. Update analysis prompts
|
||||
3. Test with platform-specific content
|
||||
|
||||
### Enhancing AI Analysis
|
||||
|
||||
1. Modify prompts in `_create_analysis_prompt()`
|
||||
2. Add new scoring criteria
|
||||
3. Implement additional recommendation types
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**No Predictions Generated**:
|
||||
- Check LLM service availability
|
||||
- Verify content input format
|
||||
- Ensure platform is supported
|
||||
|
||||
**Low Accuracy Scores**:
|
||||
- Content may be too short/long for platform
|
||||
- Platform mismatch with content style
|
||||
- Generic content without specific appeal
|
||||
|
||||
**UI Not Loading**:
|
||||
- Check Streamlit dependencies
|
||||
- Verify import paths
|
||||
- Ensure LLM service is configured
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable detailed logging:
|
||||
```python
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
```
|
||||
|
||||
## 📈 Performance Tips
|
||||
|
||||
1. **Content Length**: Follow platform-specific optimal lengths
|
||||
2. **Platform Selection**: Choose the right platform for your content type
|
||||
3. **Target Audience**: Specify your audience for better predictions
|
||||
4. **Iterate**: Use recommendations to improve content before posting
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test with different content types
|
||||
5. Submit a pull request
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# No additional setup required!
|
||||
# Uses existing Alwrity infrastructure
|
||||
```
|
||||
|
||||
## 📝 License
|
||||
|
||||
Part of the Alwrity AI Content Creation Suite.
|
||||
|
||||
---
|
||||
|
||||
**Ready to predict your content's success? Access the AI Content Performance Predictor through the AI Writer Dashboard now!**
|
||||
@@ -1,662 +0,0 @@
|
||||
"""
|
||||
AI-Powered Content Performance Predictor
|
||||
|
||||
This module uses AI (LLM) to predict content performance instead of traditional ML models.
|
||||
Perfect for solo developers who want competitive intelligence without expensive ML infrastructure.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List, Optional
|
||||
from loguru import logger
|
||||
import streamlit as st
|
||||
|
||||
# Import existing Alwrity modules
|
||||
from lib.database.twitter_service import TwitterDatabaseService
|
||||
from lib.ai_web_researcher.google_trends_researcher import do_google_trends_analysis
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
class AIContentPerformancePredictor:
|
||||
"""
|
||||
AI-powered content performance predictor using LLM intelligence.
|
||||
No ML training required - uses AI's existing knowledge of content patterns.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the AI predictor."""
|
||||
self.twitter_service = TwitterDatabaseService()
|
||||
self.platform_configs = {
|
||||
'twitter': {
|
||||
'optimal_length': 120,
|
||||
'hashtag_range': (1, 3),
|
||||
'best_times': [9, 12, 15, 18, 21],
|
||||
'engagement_factors': ['questions', 'hashtags', 'mentions', 'visuals']
|
||||
},
|
||||
'linkedin': {
|
||||
'optimal_length': 1500,
|
||||
'hashtag_range': (3, 7),
|
||||
'best_times': [8, 12, 17],
|
||||
'engagement_factors': ['professional_insights', 'industry_expertise', 'networking']
|
||||
},
|
||||
'facebook': {
|
||||
'optimal_length': 200,
|
||||
'hashtag_range': (1, 5),
|
||||
'best_times': [12, 15, 18],
|
||||
'engagement_factors': ['visual_content', 'community_building', 'emotional_connection']
|
||||
},
|
||||
'instagram': {
|
||||
'optimal_length': 150,
|
||||
'hashtag_range': (5, 15),
|
||||
'best_times': [11, 13, 17, 19],
|
||||
'engagement_factors': ['visual_appeal', 'storytelling', 'trending_hashtags']
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("AI Content Performance Predictor initialized")
|
||||
|
||||
async def predict_content_performance(self, content_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Predict content performance using AI analysis.
|
||||
|
||||
Args:
|
||||
content_data: Dictionary containing content and metadata
|
||||
|
||||
Returns:
|
||||
AI-powered performance prediction with insights
|
||||
"""
|
||||
try:
|
||||
st.info("🧠 AI is analyzing your content...")
|
||||
|
||||
# Extract content details
|
||||
content = content_data.get('content', '')
|
||||
platform = content_data.get('platform', 'twitter')
|
||||
hashtags = content_data.get('hashtags', [])
|
||||
posting_time = content_data.get('posting_time', datetime.now())
|
||||
|
||||
# Get current trends for context
|
||||
trending_context = await self._get_trending_context(platform)
|
||||
|
||||
# Create comprehensive AI prompt for prediction
|
||||
prediction_prompt = self._create_prediction_prompt(
|
||||
content, platform, hashtags, posting_time, trending_context
|
||||
)
|
||||
|
||||
# Get AI prediction
|
||||
ai_response = llm_text_gen(
|
||||
prediction_prompt,
|
||||
system_prompt="You are an expert social media analyst with deep knowledge of content performance patterns across all platforms. Provide specific, actionable predictions."
|
||||
)
|
||||
|
||||
# Parse AI response into structured prediction
|
||||
structured_prediction = self._parse_ai_prediction(ai_response, content_data)
|
||||
|
||||
# Add platform-specific insights
|
||||
platform_insights = self._get_platform_insights(content_data, platform)
|
||||
|
||||
# Generate actionable recommendations
|
||||
recommendations = await self._generate_ai_recommendations(content_data, structured_prediction)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'content_analyzed': content[:100] + "..." if len(content) > 100 else content,
|
||||
'platform': platform,
|
||||
'ai_prediction': structured_prediction,
|
||||
'platform_insights': platform_insights,
|
||||
'recommendations': recommendations,
|
||||
'trending_context': trending_context,
|
||||
'analysis_timestamp': datetime.now().isoformat(),
|
||||
'confidence_level': self._calculate_confidence_level(content_data)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error in AI prediction: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {'error': error_msg}
|
||||
|
||||
def _create_prediction_prompt(
|
||||
self,
|
||||
content: str,
|
||||
platform: str,
|
||||
hashtags: List[str],
|
||||
posting_time: datetime,
|
||||
trending_context: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create a comprehensive prompt for AI prediction."""
|
||||
|
||||
config = self.platform_configs.get(platform, {})
|
||||
|
||||
prompt = f"""
|
||||
Analyze this {platform} content and predict its performance:
|
||||
|
||||
CONTENT TO ANALYZE:
|
||||
"{content}"
|
||||
|
||||
METADATA:
|
||||
- Platform: {platform}
|
||||
- Hashtags: {hashtags}
|
||||
- Posting Time: {posting_time.strftime('%A %I:%M %p')}
|
||||
- Content Length: {len(content)} characters
|
||||
- Word Count: {len(content.split())} words
|
||||
|
||||
PLATFORM CONTEXT:
|
||||
- Optimal Length: {config.get('optimal_length', 'N/A')} characters
|
||||
- Recommended Hashtags: {config.get('hashtag_range', 'N/A')}
|
||||
- Best Posting Times: {config.get('best_times', 'N/A')}
|
||||
|
||||
CURRENT TRENDS:
|
||||
{json.dumps(trending_context, indent=2)}
|
||||
|
||||
PREDICTION REQUIREMENTS:
|
||||
Please provide a detailed analysis with these specific predictions:
|
||||
|
||||
1. ENGAGEMENT PREDICTION:
|
||||
- Estimated engagement rate (0-10%)
|
||||
- Estimated likes (number)
|
||||
- Estimated shares/retweets (number)
|
||||
- Estimated comments (number)
|
||||
|
||||
2. PERFORMANCE ANALYSIS:
|
||||
- Strengths of this content
|
||||
- Weaknesses to address
|
||||
- Viral potential (Low/Medium/High)
|
||||
- Audience appeal rating (1-10)
|
||||
|
||||
3. OPTIMIZATION OPPORTUNITIES:
|
||||
- How to improve engagement potential
|
||||
- Better hashtag suggestions
|
||||
- Content format improvements
|
||||
- Timing optimization
|
||||
|
||||
4. COMPETITIVE ASSESSMENT:
|
||||
- How this compares to typical content in this niche
|
||||
- Unique elements that stand out
|
||||
- Missing elements competitors usually include
|
||||
|
||||
Format your response as a detailed analysis with specific numbers and actionable insights.
|
||||
Be realistic but optimistic in your predictions.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
|
||||
async def _get_trending_context(self, platform: str) -> Dict[str, Any]:
|
||||
"""Get current trending context for better predictions."""
|
||||
try:
|
||||
# Use existing Twitter integration if available
|
||||
if platform == 'twitter' and hasattr(self.twitter_service, 'get_trending_topics'):
|
||||
trending_topics = self.twitter_service.get_trending_topics()
|
||||
else:
|
||||
# Fallback to general trends
|
||||
trending_topics = [
|
||||
'AI and technology',
|
||||
'Content creation',
|
||||
'Social media marketing',
|
||||
'Digital transformation',
|
||||
'Remote work'
|
||||
]
|
||||
|
||||
return {
|
||||
'trending_topics': trending_topics[:5],
|
||||
'platform': platform,
|
||||
'analysis_date': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting trending context: {str(e)}")
|
||||
return {
|
||||
'trending_topics': ['General content', 'Engagement tips'],
|
||||
'platform': platform,
|
||||
'analysis_date': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def _parse_ai_prediction(self, ai_response: str, content_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Parse AI response into structured prediction data."""
|
||||
try:
|
||||
# Extract numerical predictions using simple parsing
|
||||
# This is a simplified version - in production, you might want more sophisticated parsing
|
||||
|
||||
prediction = {
|
||||
'engagement_rate': self._extract_percentage(ai_response, 'engagement rate'),
|
||||
'estimated_likes': self._extract_number(ai_response, 'likes'),
|
||||
'estimated_shares': self._extract_number(ai_response, ['shares', 'retweets']),
|
||||
'estimated_comments': self._extract_number(ai_response, 'comments'),
|
||||
'viral_potential': self._extract_rating(ai_response, 'viral potential'),
|
||||
'audience_appeal': self._extract_rating(ai_response, 'audience appeal'),
|
||||
'strengths': self._extract_list_items(ai_response, 'strengths'),
|
||||
'weaknesses': self._extract_list_items(ai_response, 'weaknesses'),
|
||||
'full_analysis': ai_response
|
||||
}
|
||||
|
||||
return prediction
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing AI prediction: {str(e)}")
|
||||
return {
|
||||
'engagement_rate': 2.5, # Default reasonable prediction
|
||||
'estimated_likes': 50,
|
||||
'estimated_shares': 10,
|
||||
'estimated_comments': 5,
|
||||
'viral_potential': 'Medium',
|
||||
'audience_appeal': 7,
|
||||
'full_analysis': ai_response
|
||||
}
|
||||
|
||||
def _get_platform_insights(self, content_data: Dict[str, Any], platform: str) -> Dict[str, Any]:
|
||||
"""Get platform-specific insights."""
|
||||
config = self.platform_configs.get(platform, {})
|
||||
content = content_data.get('content', '')
|
||||
hashtags = content_data.get('hashtags', [])
|
||||
|
||||
insights = {
|
||||
'platform_optimization': [],
|
||||
'timing_analysis': {},
|
||||
'format_analysis': {},
|
||||
'hashtag_analysis': {}
|
||||
}
|
||||
|
||||
# Length analysis
|
||||
optimal_length = config.get('optimal_length', 200)
|
||||
current_length = len(content)
|
||||
|
||||
if abs(current_length - optimal_length) > 50:
|
||||
insights['platform_optimization'].append(
|
||||
f"Content length ({current_length}) differs from optimal ({optimal_length}) for {platform}"
|
||||
)
|
||||
else:
|
||||
insights['platform_optimization'].append(
|
||||
f"Content length is well-optimized for {platform}"
|
||||
)
|
||||
|
||||
# Hashtag analysis
|
||||
hashtag_range = config.get('hashtag_range', (1, 5))
|
||||
hashtag_count = len(hashtags)
|
||||
|
||||
if hashtag_count < hashtag_range[0]:
|
||||
insights['hashtag_analysis']['recommendation'] = f"Add more hashtags (optimal: {hashtag_range[0]}-{hashtag_range[1]})"
|
||||
elif hashtag_count > hashtag_range[1]:
|
||||
insights['hashtag_analysis']['recommendation'] = f"Consider reducing hashtags (optimal: {hashtag_range[0]}-{hashtag_range[1]})"
|
||||
else:
|
||||
insights['hashtag_analysis']['recommendation'] = "Hashtag count is optimal"
|
||||
|
||||
# Timing analysis
|
||||
best_times = config.get('best_times', [])
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
insights['timing_analysis'] = {
|
||||
'best_times': best_times,
|
||||
'current_timing': 'Optimal' if current_hour in best_times else 'Suboptimal',
|
||||
'suggestion': f"Consider posting at {best_times} for better engagement" if current_hour not in best_times else "Current timing is optimal"
|
||||
}
|
||||
|
||||
return insights
|
||||
|
||||
async def _generate_ai_recommendations(
|
||||
self,
|
||||
content_data: Dict[str, Any],
|
||||
prediction: Dict[str, Any]
|
||||
) -> List[Dict[str, str]]:
|
||||
"""Generate AI-powered recommendations for improvement."""
|
||||
|
||||
recommendations_prompt = f"""
|
||||
Based on this content analysis, provide specific improvement recommendations:
|
||||
|
||||
CONTENT: "{content_data.get('content', '')[:200]}..."
|
||||
PLATFORM: {content_data.get('platform', 'twitter')}
|
||||
PREDICTED ENGAGEMENT: {prediction.get('engagement_rate', 'N/A')}%
|
||||
VIRAL POTENTIAL: {prediction.get('viral_potential', 'N/A')}
|
||||
|
||||
Provide 5-7 specific, actionable recommendations to improve this content's performance:
|
||||
|
||||
1. Content optimization suggestions
|
||||
2. Hashtag improvements
|
||||
3. Timing recommendations
|
||||
4. Format enhancements
|
||||
5. Engagement boosters
|
||||
6. Audience targeting tips
|
||||
7. Platform-specific optimizations
|
||||
|
||||
Format each recommendation as:
|
||||
- Category: [category]
|
||||
- Action: [specific action to take]
|
||||
- Expected Impact: [what improvement to expect]
|
||||
- Priority: [High/Medium/Low]
|
||||
|
||||
Focus on quick wins and high-impact changes.
|
||||
"""
|
||||
|
||||
try:
|
||||
ai_recommendations = llm_text_gen(
|
||||
recommendations_prompt,
|
||||
system_prompt="You are a content optimization expert. Provide specific, actionable recommendations that can be implemented immediately."
|
||||
)
|
||||
|
||||
# Parse recommendations into structured format
|
||||
return self._parse_recommendations(ai_recommendations)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating AI recommendations: {str(e)}")
|
||||
return [
|
||||
{
|
||||
'category': 'Content Enhancement',
|
||||
'action': 'Add more engaging elements like questions or calls-to-action',
|
||||
'expected_impact': 'Increase engagement by 20-30%',
|
||||
'priority': 'High'
|
||||
},
|
||||
{
|
||||
'category': 'Hashtag Optimization',
|
||||
'action': 'Research and add 2-3 trending relevant hashtags',
|
||||
'expected_impact': 'Improve discoverability',
|
||||
'priority': 'Medium'
|
||||
}
|
||||
]
|
||||
|
||||
def _extract_percentage(self, text: str, keyword: str) -> float:
|
||||
"""Extract percentage value from AI response."""
|
||||
import re
|
||||
patterns = [
|
||||
rf'{keyword}.*?(\d+\.?\d*)%',
|
||||
rf'(\d+\.?\d*)%.*?{keyword}',
|
||||
rf'{keyword}.*?(\d+\.?\d*) percent'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text, re.IGNORECASE)
|
||||
if match:
|
||||
return float(match.group(1))
|
||||
|
||||
return 2.5 # Default reasonable engagement rate
|
||||
|
||||
def _extract_number(self, text: str, keywords: List[str]) -> int:
|
||||
"""Extract number from AI response."""
|
||||
import re
|
||||
|
||||
if isinstance(keywords, str):
|
||||
keywords = [keywords]
|
||||
|
||||
for keyword in keywords:
|
||||
patterns = [
|
||||
rf'{keyword}.*?(\d+)',
|
||||
rf'(\d+).*?{keyword}'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text, re.IGNORECASE)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
|
||||
return 25 # Default reasonable number
|
||||
|
||||
def _extract_rating(self, text: str, keyword: str) -> str:
|
||||
"""Extract rating (High/Medium/Low) from AI response."""
|
||||
import re
|
||||
|
||||
pattern = rf'{keyword}.*?(High|Medium|Low)'
|
||||
match = re.search(pattern, text, re.IGNORECASE)
|
||||
|
||||
if match:
|
||||
return match.group(1).capitalize()
|
||||
|
||||
return 'Medium' # Default
|
||||
|
||||
def _extract_list_items(self, text: str, section: str) -> List[str]:
|
||||
"""Extract list items from a section of AI response."""
|
||||
import re
|
||||
|
||||
# Find the section
|
||||
section_pattern = rf'{section}:?\s*(.*?)(?=\n\n|\d\.|[A-Z]+:|$)'
|
||||
match = re.search(section_pattern, text, re.IGNORECASE | re.DOTALL)
|
||||
|
||||
if match:
|
||||
section_text = match.group(1)
|
||||
# Extract bullet points or numbered items
|
||||
items = re.findall(r'[-•]\s*(.+)', section_text)
|
||||
if not items:
|
||||
items = re.findall(r'\d+\.\s*(.+)', section_text)
|
||||
|
||||
return [item.strip() for item in items[:3]] # Return first 3 items
|
||||
|
||||
return []
|
||||
|
||||
def _parse_recommendations(self, ai_recommendations: str) -> List[Dict[str, str]]:
|
||||
"""Parse AI recommendations into structured format."""
|
||||
recommendations = []
|
||||
|
||||
try:
|
||||
# Simple parsing - split by numbers or bullet points
|
||||
import re
|
||||
sections = re.split(r'\d+\.|[-•]', ai_recommendations)
|
||||
|
||||
for section in sections[1:6]: # Take first 5 recommendations
|
||||
if len(section.strip()) > 10: # Only substantial recommendations
|
||||
recommendations.append({
|
||||
'category': 'AI Recommendation',
|
||||
'action': section.strip()[:200], # Limit length
|
||||
'expected_impact': 'Improved engagement',
|
||||
'priority': 'Medium'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing recommendations: {str(e)}")
|
||||
|
||||
# Ensure we have at least a few recommendations
|
||||
if len(recommendations) < 3:
|
||||
recommendations.extend([
|
||||
{
|
||||
'category': 'Engagement',
|
||||
'action': 'Add questions to encourage audience interaction',
|
||||
'expected_impact': '20-30% more engagement',
|
||||
'priority': 'High'
|
||||
},
|
||||
{
|
||||
'category': 'Visibility',
|
||||
'action': 'Use trending hashtags relevant to your niche',
|
||||
'expected_impact': 'Better discoverability',
|
||||
'priority': 'Medium'
|
||||
},
|
||||
{
|
||||
'category': 'Timing',
|
||||
'action': 'Post during peak engagement hours for your audience',
|
||||
'expected_impact': '15-25% more reach',
|
||||
'priority': 'Medium'
|
||||
}
|
||||
])
|
||||
|
||||
return recommendations[:7] # Return max 7 recommendations
|
||||
|
||||
def _calculate_confidence_level(self, content_data: Dict[str, Any]) -> str:
|
||||
"""Calculate confidence level of prediction."""
|
||||
confidence_factors = 0
|
||||
|
||||
# More complete data = higher confidence
|
||||
if content_data.get('content'):
|
||||
confidence_factors += 1
|
||||
if content_data.get('hashtags'):
|
||||
confidence_factors += 1
|
||||
if content_data.get('platform'):
|
||||
confidence_factors += 1
|
||||
if content_data.get('posting_time'):
|
||||
confidence_factors += 1
|
||||
|
||||
if confidence_factors >= 4:
|
||||
return 'High'
|
||||
elif confidence_factors >= 2:
|
||||
return 'Medium'
|
||||
else:
|
||||
return 'Low'
|
||||
|
||||
async def analyze_content_batch(self, content_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Analyze multiple pieces of content."""
|
||||
results = []
|
||||
|
||||
for i, content_data in enumerate(content_list):
|
||||
st.write(f"🔍 Analyzing content {i+1}/{len(content_list)}")
|
||||
result = await self.predict_content_performance(content_data)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
def get_platform_best_practices(self, platform: str) -> Dict[str, Any]:
|
||||
"""Get best practices for a specific platform."""
|
||||
config = self.platform_configs.get(platform, {})
|
||||
|
||||
return {
|
||||
'platform': platform,
|
||||
'optimal_length': config.get('optimal_length'),
|
||||
'hashtag_range': config.get('hashtag_range'),
|
||||
'best_posting_times': config.get('best_times'),
|
||||
'engagement_factors': config.get('engagement_factors', []),
|
||||
'tips': [
|
||||
f"Keep content around {config.get('optimal_length', 200)} characters",
|
||||
f"Use {config.get('hashtag_range', (1, 5))[0]}-{config.get('hashtag_range', (1, 5))[1]} relevant hashtags",
|
||||
f"Post during peak hours: {config.get('best_times', [])}",
|
||||
"Include engaging elements like questions or calls-to-action",
|
||||
"Use visuals when possible to increase engagement"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Usage example and Streamlit interface
|
||||
def render_ai_predictor_ui():
|
||||
"""Render the AI content performance predictor interface."""
|
||||
st.title("🎯 AI Content Performance Predictor")
|
||||
st.markdown("Get AI-powered predictions for your content performance - no ML training required!")
|
||||
|
||||
# Initialize predictor
|
||||
if 'ai_predictor' not in st.session_state:
|
||||
st.session_state.ai_predictor = AIContentPerformancePredictor()
|
||||
|
||||
predictor = st.session_state.ai_predictor
|
||||
|
||||
# Input section
|
||||
st.header("📝 Content Analysis")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
platform = st.selectbox(
|
||||
"Platform",
|
||||
["twitter", "linkedin", "facebook", "instagram"],
|
||||
help="Choose your target platform"
|
||||
)
|
||||
|
||||
posting_time = st.time_input("Posting Time", value=datetime.now().time())
|
||||
|
||||
with col2:
|
||||
hashtags_input = st.text_input(
|
||||
"Hashtags (comma-separated)",
|
||||
value="AI, ContentCreation, Marketing",
|
||||
help="Enter hashtags without # symbol"
|
||||
)
|
||||
|
||||
content = st.text_area(
|
||||
"Content to Analyze",
|
||||
value="Discover how AI is revolutionizing content creation! What's your experience with AI tools? Share your thoughts below! 🚀",
|
||||
height=150,
|
||||
help="Enter the content you want to analyze"
|
||||
)
|
||||
|
||||
# Process hashtags
|
||||
hashtags = [tag.strip() for tag in hashtags_input.split(',') if tag.strip()]
|
||||
|
||||
if st.button("🧠 Analyze Content Performance", type="primary"):
|
||||
if content:
|
||||
# Prepare content data
|
||||
content_data = {
|
||||
'content': content,
|
||||
'platform': platform,
|
||||
'hashtags': hashtags,
|
||||
'posting_time': datetime.combine(datetime.now().date(), posting_time)
|
||||
}
|
||||
|
||||
# Run AI analysis
|
||||
with st.spinner("🤖 AI is analyzing your content..."):
|
||||
results = asyncio.run(predictor.predict_content_performance(content_data))
|
||||
|
||||
if results.get('success'):
|
||||
st.success("✅ Analysis Complete!")
|
||||
|
||||
# Display predictions
|
||||
st.header("📊 AI Performance Prediction")
|
||||
|
||||
prediction = results.get('ai_prediction', {})
|
||||
|
||||
# Key metrics
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric(
|
||||
"Engagement Rate",
|
||||
f"{prediction.get('engagement_rate', 0):.1f}%"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.metric(
|
||||
"Est. Likes",
|
||||
f"{prediction.get('estimated_likes', 0):,}"
|
||||
)
|
||||
|
||||
with col3:
|
||||
st.metric(
|
||||
"Est. Shares",
|
||||
f"{prediction.get('estimated_shares', 0):,}"
|
||||
)
|
||||
|
||||
with col4:
|
||||
st.metric(
|
||||
"Viral Potential",
|
||||
prediction.get('viral_potential', 'Medium')
|
||||
)
|
||||
|
||||
# Platform insights
|
||||
platform_insights = results.get('platform_insights', {})
|
||||
if platform_insights:
|
||||
st.subheader("🎯 Platform Optimization")
|
||||
|
||||
for insight in platform_insights.get('platform_optimization', []):
|
||||
st.info(f"💡 {insight}")
|
||||
|
||||
# Timing analysis
|
||||
timing = platform_insights.get('timing_analysis', {})
|
||||
if timing:
|
||||
st.write(f"**Timing Analysis:** {timing.get('suggestion', 'N/A')}")
|
||||
|
||||
# Hashtag analysis
|
||||
hashtag_analysis = platform_insights.get('hashtag_analysis', {})
|
||||
if hashtag_analysis:
|
||||
st.write(f"**Hashtag Recommendation:** {hashtag_analysis.get('recommendation', 'N/A')}")
|
||||
|
||||
# AI Recommendations
|
||||
recommendations = results.get('recommendations', [])
|
||||
if recommendations:
|
||||
st.subheader("🚀 AI Recommendations")
|
||||
|
||||
for i, rec in enumerate(recommendations):
|
||||
with st.expander(f"💡 {rec.get('category', 'Recommendation')} - {rec.get('priority', 'Medium')} Priority"):
|
||||
st.write(f"**Action:** {rec.get('action', 'N/A')}")
|
||||
st.write(f"**Expected Impact:** {rec.get('expected_impact', 'N/A')}")
|
||||
|
||||
# Full AI Analysis
|
||||
if prediction.get('full_analysis'):
|
||||
with st.expander("🤖 Complete AI Analysis"):
|
||||
st.write(prediction['full_analysis'])
|
||||
|
||||
else:
|
||||
st.error(f"❌ Analysis failed: {results.get('error')}")
|
||||
else:
|
||||
st.warning("⚠️ Please enter content to analyze")
|
||||
|
||||
# Platform best practices
|
||||
st.sidebar.header("📚 Platform Best Practices")
|
||||
selected_platform = st.sidebar.selectbox("Get tips for:", ["twitter", "linkedin", "facebook", "instagram"])
|
||||
|
||||
best_practices = predictor.get_platform_best_practices(selected_platform)
|
||||
|
||||
st.sidebar.write(f"**{selected_platform.title()} Best Practices:**")
|
||||
for tip in best_practices.get('tips', []):
|
||||
st.sidebar.write(f"• {tip}")
|
||||
|
||||
|
||||
# Main execution
|
||||
if __name__ == "__main__":
|
||||
render_ai_predictor_ui()
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
UI module for the Content Scheduler dashboard.
|
||||
"""
|
||||
|
||||
from .dashboard import run_dashboard
|
||||
|
||||
__all__ = ['run_dashboard']
|
||||
@@ -1,386 +0,0 @@
|
||||
"""
|
||||
Main dashboard implementation for the Content Scheduler.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from lib.database.models import ContentItem, Schedule, ScheduleStatus, ContentType, Platform, get_engine, get_session, init_db
|
||||
|
||||
engine = get_engine()
|
||||
init_db(engine)
|
||||
session = get_session(engine)
|
||||
|
||||
def run_dashboard():
|
||||
"""Run the Streamlit dashboard."""
|
||||
|
||||
st.title("📅 Alwrity Content Scheduler Dashboard")
|
||||
|
||||
# Sidebar navigation
|
||||
st.sidebar.title("Navigation")
|
||||
page = st.sidebar.radio(
|
||||
"Go to",
|
||||
["Overview", "Schedule Management", "Create Schedule", "Job Monitor", "Analytics"]
|
||||
)
|
||||
|
||||
if page == "Overview":
|
||||
show_overview()
|
||||
elif page == "Schedule Management":
|
||||
show_schedule_management()
|
||||
elif page == "Create Schedule":
|
||||
show_create_schedule()
|
||||
elif page == "Job Monitor":
|
||||
show_job_monitor()
|
||||
else:
|
||||
show_analytics()
|
||||
|
||||
def show_overview():
|
||||
"""Display the overview dashboard."""
|
||||
st.header("📊 Overview")
|
||||
|
||||
# Get data from unified database
|
||||
all_content = session.query(ContentItem).all()
|
||||
all_schedules = session.query(Schedule).all()
|
||||
|
||||
# Display metrics
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Total Content Items", len(all_content))
|
||||
|
||||
with col2:
|
||||
scheduled_count = len([s for s in all_schedules if s.status == ScheduleStatus.SCHEDULED])
|
||||
st.metric("Scheduled Items", scheduled_count)
|
||||
|
||||
with col3:
|
||||
completed_count = len([s for s in all_schedules if s.status == ScheduleStatus.COMPLETED])
|
||||
st.metric("Completed", completed_count)
|
||||
|
||||
with col4:
|
||||
failed_count = len([s for s in all_schedules if s.status == ScheduleStatus.FAILED])
|
||||
st.metric("Failed", failed_count)
|
||||
|
||||
# Recent content
|
||||
st.subheader("📝 Recent Content Items")
|
||||
if all_content:
|
||||
recent_content = sorted(all_content, key=lambda x: x.created_at, reverse=True)[:5]
|
||||
for item in recent_content:
|
||||
with st.expander(f"{item.title} ({item.content_type.value})"):
|
||||
st.write(f"**Description:** {item.description or 'No description'}")
|
||||
st.write(f"**Platforms:** {', '.join(item.platforms) if isinstance(item.platforms, list) else item.platforms}")
|
||||
st.write(f"**Status:** {item.status}")
|
||||
st.write(f"**Created:** {item.created_at}")
|
||||
|
||||
# Show associated schedules
|
||||
item_schedules = [s for s in all_schedules if s.content_item_id == item.id]
|
||||
if item_schedules:
|
||||
st.write("**Schedules:**")
|
||||
for schedule in item_schedules:
|
||||
st.write(f" - {schedule.scheduled_time} ({schedule.status.value})")
|
||||
else:
|
||||
st.info("No content items found. Create some content in the Content Calendar first!")
|
||||
|
||||
def show_schedule_management():
|
||||
"""Display the schedule management interface."""
|
||||
st.header("📅 Schedule Management")
|
||||
|
||||
# Get all schedules
|
||||
all_schedules = session.query(Schedule).all()
|
||||
|
||||
if not all_schedules:
|
||||
st.info("No schedules found. Create schedules from the 'Create Schedule' tab.")
|
||||
return
|
||||
|
||||
# Filter options
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
status_filter = st.selectbox(
|
||||
"Filter by Status",
|
||||
options=["All"] + [status.value for status in ScheduleStatus],
|
||||
key="schedule_status_filter"
|
||||
)
|
||||
|
||||
with col2:
|
||||
date_filter = st.date_input(
|
||||
"Filter by Date (from)",
|
||||
value=datetime.now().date() - timedelta(days=30),
|
||||
key="schedule_date_filter"
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
filtered_schedules = all_schedules
|
||||
if status_filter != "All":
|
||||
filtered_schedules = [s for s in filtered_schedules if s.status.value == status_filter]
|
||||
|
||||
filtered_schedules = [s for s in filtered_schedules if s.scheduled_time.date() >= date_filter]
|
||||
|
||||
# Display schedules
|
||||
st.subheader(f"📋 Schedules ({len(filtered_schedules)} items)")
|
||||
|
||||
for schedule in sorted(filtered_schedules, key=lambda x: x.scheduled_time, reverse=True):
|
||||
content_item = session.query(ContentItem).get(schedule.content_item_id)
|
||||
|
||||
if content_item:
|
||||
with st.expander(f"{content_item.title} - {schedule.scheduled_time.strftime('%Y-%m-%d %H:%M')} ({schedule.status.value})"):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.write(f"**Content:** {content_item.title}")
|
||||
st.write(f"**Type:** {content_item.content_type.value}")
|
||||
st.write(f"**Platforms:** {', '.join(content_item.platforms) if isinstance(content_item.platforms, list) else content_item.platforms}")
|
||||
st.write(f"**Scheduled Time:** {schedule.scheduled_time}")
|
||||
st.write(f"**Status:** {schedule.status.value}")
|
||||
|
||||
with col2:
|
||||
st.write(f"**Recurrence:** {schedule.recurrence or 'One-time'}")
|
||||
st.write(f"**Priority:** {schedule.priority}")
|
||||
st.write(f"**Created:** {schedule.created_at}")
|
||||
if schedule.result:
|
||||
st.write(f"**Result:** {schedule.result}")
|
||||
|
||||
# Action buttons
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
if st.button(f"Edit Schedule", key=f"edit_{schedule.id}"):
|
||||
st.session_state.edit_schedule_id = schedule.id
|
||||
st.rerun()
|
||||
|
||||
with col2:
|
||||
if schedule.status == ScheduleStatus.SCHEDULED:
|
||||
if st.button(f"Cancel", key=f"cancel_{schedule.id}"):
|
||||
schedule.status = ScheduleStatus.CANCELLED
|
||||
session.commit()
|
||||
st.success("Schedule cancelled!")
|
||||
st.rerun()
|
||||
|
||||
with col3:
|
||||
if st.button(f"Delete", key=f"delete_{schedule.id}"):
|
||||
session.delete(schedule)
|
||||
session.commit()
|
||||
st.success("Schedule deleted!")
|
||||
st.rerun()
|
||||
|
||||
def show_create_schedule():
|
||||
"""Display the schedule creation interface."""
|
||||
st.header("➕ Create New Schedule")
|
||||
|
||||
# Get available content items
|
||||
content_items = session.query(ContentItem).all()
|
||||
|
||||
if not content_items:
|
||||
st.warning("No content items available. Please create content in the Content Calendar first.")
|
||||
return
|
||||
|
||||
# Create schedule form
|
||||
with st.form("create_schedule_form"):
|
||||
st.subheader("Schedule Configuration")
|
||||
|
||||
# Select content item
|
||||
content_options = {f"{item.title} ({item.content_type.value})": item.id for item in content_items}
|
||||
selected_content = st.selectbox(
|
||||
"Select Content Item",
|
||||
options=list(content_options.keys()),
|
||||
key="schedule_content_select"
|
||||
)
|
||||
|
||||
# Schedule timing
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
schedule_date = st.date_input(
|
||||
"Schedule Date",
|
||||
value=datetime.now().date() + timedelta(days=1),
|
||||
key="schedule_date"
|
||||
)
|
||||
|
||||
with col2:
|
||||
schedule_time = st.time_input(
|
||||
"Schedule Time",
|
||||
value=datetime.now().time(),
|
||||
key="schedule_time"
|
||||
)
|
||||
|
||||
# Combine date and time
|
||||
schedule_datetime = datetime.combine(schedule_date, schedule_time)
|
||||
|
||||
# Recurrence options
|
||||
recurrence = st.selectbox(
|
||||
"Recurrence",
|
||||
options=["none", "daily", "weekly", "monthly"],
|
||||
key="schedule_recurrence"
|
||||
)
|
||||
|
||||
# Priority
|
||||
priority = st.slider(
|
||||
"Priority",
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
value=5,
|
||||
key="schedule_priority"
|
||||
)
|
||||
|
||||
# Platform selection (override content item platforms if needed)
|
||||
content_item_id = content_options[selected_content]
|
||||
content_item = session.query(ContentItem).get(content_item_id)
|
||||
|
||||
if content_item:
|
||||
current_platforms = content_item.platforms if isinstance(content_item.platforms, list) else [content_item.platforms]
|
||||
st.write(f"**Current Platforms:** {', '.join(current_platforms)}")
|
||||
|
||||
override_platforms = st.checkbox("Override Platforms", key="override_platforms")
|
||||
|
||||
if override_platforms:
|
||||
available_platforms = [p.value for p in Platform]
|
||||
selected_platforms = st.multiselect(
|
||||
"Select Platforms",
|
||||
options=available_platforms,
|
||||
default=current_platforms,
|
||||
key="schedule_platforms"
|
||||
)
|
||||
else:
|
||||
selected_platforms = current_platforms
|
||||
|
||||
# Submit button
|
||||
submitted = st.form_submit_button("Create Schedule")
|
||||
|
||||
if submitted:
|
||||
try:
|
||||
# Create new schedule
|
||||
new_schedule = Schedule(
|
||||
content_item_id=content_item_id,
|
||||
scheduled_time=schedule_datetime,
|
||||
status=ScheduleStatus.SCHEDULED,
|
||||
recurrence=recurrence if recurrence != "none" else None,
|
||||
priority=priority
|
||||
)
|
||||
|
||||
session.add(new_schedule)
|
||||
session.commit()
|
||||
|
||||
st.success(f"✅ Schedule created successfully! Content will be published on {schedule_datetime}")
|
||||
|
||||
# Show schedule details
|
||||
with st.expander("Schedule Details", expanded=True):
|
||||
st.write(f"**Content:** {content_item.title}")
|
||||
st.write(f"**Scheduled Time:** {schedule_datetime}")
|
||||
st.write(f"**Platforms:** {', '.join(selected_platforms)}")
|
||||
st.write(f"**Recurrence:** {recurrence}")
|
||||
st.write(f"**Priority:** {priority}")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ Error creating schedule: {str(e)}")
|
||||
|
||||
def show_job_monitor():
|
||||
"""Display the job monitoring interface."""
|
||||
st.header("🔍 Job Monitor")
|
||||
|
||||
# Get all schedules with their status
|
||||
all_schedules = session.query(Schedule).all()
|
||||
|
||||
if not all_schedules:
|
||||
st.info("No jobs to monitor.")
|
||||
return
|
||||
|
||||
# Status distribution
|
||||
status_counts = {}
|
||||
for schedule in all_schedules:
|
||||
status = schedule.status.value
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# Display status chart
|
||||
if status_counts:
|
||||
fig = px.pie(
|
||||
values=list(status_counts.values()),
|
||||
names=list(status_counts.keys()),
|
||||
title="Job Status Distribution"
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Recent job activity
|
||||
st.subheader("📊 Recent Job Activity")
|
||||
|
||||
recent_schedules = sorted(all_schedules, key=lambda x: x.updated_at, reverse=True)[:10]
|
||||
|
||||
for schedule in recent_schedules:
|
||||
content_item = session.query(ContentItem).get(schedule.content_item_id)
|
||||
|
||||
if content_item:
|
||||
status_color = {
|
||||
ScheduleStatus.SCHEDULED: "🟡",
|
||||
ScheduleStatus.RUNNING: "🔵",
|
||||
ScheduleStatus.COMPLETED: "🟢",
|
||||
ScheduleStatus.FAILED: "🔴",
|
||||
ScheduleStatus.CANCELLED: "⚫"
|
||||
}.get(schedule.status, "⚪")
|
||||
|
||||
st.write(f"{status_color} **{content_item.title}** - {schedule.status.value} - {schedule.updated_at.strftime('%Y-%m-%d %H:%M')}")
|
||||
|
||||
if schedule.result:
|
||||
st.write(f" └─ {schedule.result}")
|
||||
|
||||
def show_analytics():
|
||||
"""Display the analytics dashboard."""
|
||||
st.header("📈 Analytics")
|
||||
|
||||
# Get data
|
||||
all_content = session.query(ContentItem).all()
|
||||
all_schedules = session.query(Schedule).all()
|
||||
|
||||
if not all_schedules:
|
||||
st.info("No data available for analytics.")
|
||||
return
|
||||
|
||||
# Time-based analytics
|
||||
st.subheader("📅 Schedule Timeline")
|
||||
|
||||
# Create timeline data
|
||||
timeline_data = []
|
||||
for schedule in all_schedules:
|
||||
content_item = session.query(ContentItem).get(schedule.content_item_id)
|
||||
if content_item:
|
||||
timeline_data.append({
|
||||
'Date': schedule.scheduled_time.date(),
|
||||
'Content': content_item.title,
|
||||
'Status': schedule.status.value,
|
||||
'Type': content_item.content_type.value
|
||||
})
|
||||
|
||||
if timeline_data:
|
||||
df = pd.DataFrame(timeline_data)
|
||||
|
||||
# Schedule frequency by date
|
||||
date_counts = df.groupby('Date').size().reset_index(name='Count')
|
||||
fig = px.line(date_counts, x='Date', y='Count', title='Scheduled Content Over Time')
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Content type distribution
|
||||
type_counts = df['Type'].value_counts()
|
||||
fig = px.bar(x=type_counts.index, y=type_counts.values, title='Content Type Distribution')
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Status breakdown
|
||||
status_counts = df['Status'].value_counts()
|
||||
fig = px.pie(values=status_counts.values, names=status_counts.index, title='Status Distribution')
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Performance metrics
|
||||
st.subheader("📊 Performance Metrics")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
total_schedules = len(all_schedules)
|
||||
st.metric("Total Schedules", total_schedules)
|
||||
|
||||
with col2:
|
||||
completed_schedules = len([s for s in all_schedules if s.status == ScheduleStatus.COMPLETED])
|
||||
success_rate = (completed_schedules / total_schedules * 100) if total_schedules > 0 else 0
|
||||
st.metric("Success Rate", f"{success_rate:.1f}%")
|
||||
|
||||
with col3:
|
||||
failed_schedules = len([s for s in all_schedules if s.status == ScheduleStatus.FAILED])
|
||||
failure_rate = (failed_schedules / total_schedules * 100) if total_schedules > 0 else 0
|
||||
st.metric("Failure Rate", f"{failure_rate:.1f}%")
|
||||
@@ -1,392 +0,0 @@
|
||||
"""
|
||||
Timeline view implementation for the Content Scheduler.
|
||||
Provides interactive Gantt charts and progress tracking visualization.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import plotly.figure_factory as ff
|
||||
import plotly.graph_objects as go
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
import pandas as pd
|
||||
import json
|
||||
|
||||
# Use unified database models
|
||||
from lib.database.models import ContentItem, Schedule, ScheduleStatus, get_session
|
||||
|
||||
class TimelineView:
|
||||
"""Interactive timeline view with Gantt charts and progress tracking."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the timeline view."""
|
||||
self.session = get_session()
|
||||
|
||||
def render(self):
|
||||
"""Render the timeline view."""
|
||||
st.header("Schedule Timeline")
|
||||
|
||||
# Timeline controls
|
||||
self._render_timeline_controls()
|
||||
|
||||
# Main timeline view
|
||||
self._render_timeline()
|
||||
|
||||
# Progress tracking
|
||||
self._render_progress_tracking()
|
||||
|
||||
def _render_timeline_controls(self):
|
||||
"""Render timeline control options."""
|
||||
col1, col2, col3 = st.columns([2, 2, 1])
|
||||
|
||||
with col1:
|
||||
view_type = st.selectbox(
|
||||
"View Type",
|
||||
["Gantt Chart", "Timeline", "List View"],
|
||||
help="Select the type of timeline visualization"
|
||||
)
|
||||
|
||||
with col2:
|
||||
date_range = st.date_input(
|
||||
"Date Range",
|
||||
value=(
|
||||
datetime.now().date(),
|
||||
datetime.now().date() + timedelta(days=7)
|
||||
),
|
||||
help="Select the date range to display"
|
||||
)
|
||||
|
||||
with col3:
|
||||
if st.button("Export", help="Export timeline data"):
|
||||
self._export_timeline_data()
|
||||
|
||||
def _render_timeline(self):
|
||||
"""Render the main timeline visualization."""
|
||||
# Get schedules for the selected date range
|
||||
schedules = self._get_schedules_for_timeline()
|
||||
|
||||
if not schedules:
|
||||
st.info("No schedules found for the selected date range.")
|
||||
return
|
||||
|
||||
# Create Gantt chart data
|
||||
gantt_data = self._create_gantt_data(schedules)
|
||||
|
||||
# Create and display Gantt chart
|
||||
fig = self._create_gantt_chart(gantt_data)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Display schedule details
|
||||
self._render_schedule_details(schedules)
|
||||
|
||||
def _render_progress_tracking(self):
|
||||
"""Render progress tracking visualization."""
|
||||
st.subheader("Progress Tracking")
|
||||
|
||||
# Progress metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
self._render_progress_metric(
|
||||
"Completed",
|
||||
self._get_completed_count(),
|
||||
"green"
|
||||
)
|
||||
|
||||
with col2:
|
||||
self._render_progress_metric(
|
||||
"In Progress",
|
||||
self._get_in_progress_count(),
|
||||
"orange"
|
||||
)
|
||||
|
||||
with col3:
|
||||
self._render_progress_metric(
|
||||
"Pending",
|
||||
self._get_pending_count(),
|
||||
"blue"
|
||||
)
|
||||
|
||||
# Progress chart
|
||||
self._render_progress_chart()
|
||||
|
||||
def _get_schedules_for_timeline(self) -> List[Schedule]:
|
||||
"""Get schedules for the timeline view."""
|
||||
try:
|
||||
# Get date range from session state or use default
|
||||
if hasattr(st.session_state, 'date_range') and st.session_state.date_range:
|
||||
start_date, end_date = st.session_state.date_range
|
||||
else:
|
||||
start_date = datetime.now().date()
|
||||
end_date = start_date + timedelta(days=7)
|
||||
|
||||
# Convert to datetime
|
||||
start_datetime = datetime.combine(start_date, datetime.min.time())
|
||||
end_datetime = datetime.combine(end_date, datetime.max.time())
|
||||
|
||||
# Query schedules from unified database
|
||||
schedules = self.session.query(Schedule).filter(
|
||||
Schedule.scheduled_time >= start_datetime,
|
||||
Schedule.scheduled_time <= end_datetime
|
||||
).all()
|
||||
|
||||
return schedules
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Failed to get schedules: {str(e)}")
|
||||
return []
|
||||
|
||||
def _create_gantt_data(self, schedules: List[Schedule]) -> List[Dict[str, Any]]:
|
||||
"""Create data for Gantt chart."""
|
||||
gantt_data = []
|
||||
|
||||
for schedule in schedules:
|
||||
# Get content item details
|
||||
content_item = self.session.query(ContentItem).filter(
|
||||
ContentItem.id == schedule.content_item_id
|
||||
).first()
|
||||
|
||||
if content_item:
|
||||
# Calculate task duration
|
||||
duration = timedelta(hours=1) # Default duration
|
||||
|
||||
# Create task data
|
||||
task = {
|
||||
'Task': content_item.title[:50] + "..." if len(content_item.title) > 50 else content_item.title,
|
||||
'Start': schedule.scheduled_time,
|
||||
'Finish': schedule.scheduled_time + duration,
|
||||
'Resource': schedule.status.value,
|
||||
'Status': schedule.status.value,
|
||||
'Progress': self._calculate_progress(schedule)
|
||||
}
|
||||
|
||||
gantt_data.append(task)
|
||||
|
||||
return gantt_data
|
||||
|
||||
def _create_gantt_chart(self, gantt_data: List[Dict[str, Any]]) -> go.Figure:
|
||||
"""Create Gantt chart visualization."""
|
||||
if not gantt_data:
|
||||
# Return empty figure
|
||||
fig = go.Figure()
|
||||
fig.update_layout(
|
||||
title='Content Schedule Timeline',
|
||||
xaxis_title='Timeline',
|
||||
yaxis_title='Status',
|
||||
height=400
|
||||
)
|
||||
return fig
|
||||
|
||||
# Convert data to DataFrame
|
||||
df = pd.DataFrame(gantt_data)
|
||||
|
||||
# Create Gantt chart
|
||||
fig = ff.create_gantt(
|
||||
df,
|
||||
index_col='Resource',
|
||||
show_colorbar=True,
|
||||
group_tasks=True,
|
||||
showgrid_x=True,
|
||||
showgrid_y=True
|
||||
)
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
title='Content Schedule Timeline',
|
||||
xaxis_title='Timeline',
|
||||
yaxis_title='Status',
|
||||
height=400,
|
||||
showlegend=True
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def _render_schedule_details(self, schedules: List[Schedule]):
|
||||
"""Render detailed schedule information."""
|
||||
st.subheader("Schedule Details")
|
||||
|
||||
for schedule in schedules:
|
||||
# Get content item details
|
||||
content_item = self.session.query(ContentItem).filter(
|
||||
ContentItem.id == schedule.content_item_id
|
||||
).first()
|
||||
|
||||
if content_item:
|
||||
with st.expander(f"{content_item.title} - {schedule.status.value}"):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.write("**Schedule Information**")
|
||||
st.write(f"Content Type: {content_item.content_type.value if content_item.content_type else 'Unknown'}")
|
||||
st.write(f"Status: {schedule.status.value}")
|
||||
st.write(f"Scheduled Time: {schedule.scheduled_time}")
|
||||
st.write(f"Priority: {schedule.priority}")
|
||||
if schedule.recurrence:
|
||||
st.write(f"Recurrence: {schedule.recurrence}")
|
||||
|
||||
with col2:
|
||||
st.write("**Progress**")
|
||||
progress = self._calculate_progress(schedule)
|
||||
st.progress(progress / 100)
|
||||
st.write(f"Progress: {progress:.1f}%")
|
||||
|
||||
# Action buttons
|
||||
col2a, col2b = st.columns(2)
|
||||
with col2a:
|
||||
if st.button(f"Edit {schedule.id}", key=f"edit_{schedule.id}"):
|
||||
st.session_state.edit_schedule_id = schedule.id
|
||||
with col2b:
|
||||
if st.button(f"Cancel {schedule.id}", key=f"cancel_{schedule.id}"):
|
||||
self._cancel_schedule(schedule.id)
|
||||
|
||||
def _render_progress_metric(self, label: str, value: int, color: str):
|
||||
"""Render a progress metric."""
|
||||
st.metric(label, value)
|
||||
|
||||
def _render_progress_chart(self):
|
||||
"""Render progress chart visualization."""
|
||||
try:
|
||||
# Get progress data
|
||||
progress_data = self._get_progress_data()
|
||||
|
||||
if progress_data:
|
||||
# Create pie chart
|
||||
labels = list(progress_data.keys())
|
||||
values = list(progress_data.values())
|
||||
|
||||
fig = go.Figure(data=[go.Pie(labels=labels, values=values)])
|
||||
fig.update_layout(
|
||||
title="Schedule Status Distribution",
|
||||
height=300
|
||||
)
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
else:
|
||||
st.info("No progress data available.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error rendering progress chart: {str(e)}")
|
||||
|
||||
def _calculate_progress(self, schedule: Schedule) -> float:
|
||||
"""Calculate progress percentage for a schedule."""
|
||||
try:
|
||||
if schedule.status == ScheduleStatus.COMPLETED:
|
||||
return 100.0
|
||||
elif schedule.status == ScheduleStatus.RUNNING:
|
||||
return 50.0
|
||||
elif schedule.status == ScheduleStatus.FAILED:
|
||||
return 0.0
|
||||
else: # PENDING
|
||||
return 0.0
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error calculating progress: {str(e)}")
|
||||
return 0.0
|
||||
|
||||
def _get_completed_count(self) -> int:
|
||||
"""Get count of completed schedules."""
|
||||
try:
|
||||
return self.session.query(Schedule).filter(
|
||||
Schedule.status == ScheduleStatus.COMPLETED
|
||||
).count()
|
||||
except Exception as e:
|
||||
st.error(f"Error getting completed count: {str(e)}")
|
||||
return 0
|
||||
|
||||
def _get_in_progress_count(self) -> int:
|
||||
"""Get count of in-progress schedules."""
|
||||
try:
|
||||
return self.session.query(Schedule).filter(
|
||||
Schedule.status == ScheduleStatus.RUNNING
|
||||
).count()
|
||||
except Exception as e:
|
||||
st.error(f"Error getting in-progress count: {str(e)}")
|
||||
return 0
|
||||
|
||||
def _get_pending_count(self) -> int:
|
||||
"""Get count of pending schedules."""
|
||||
try:
|
||||
return self.session.query(Schedule).filter(
|
||||
Schedule.status == ScheduleStatus.PENDING
|
||||
).count()
|
||||
except Exception as e:
|
||||
st.error(f"Error getting pending count: {str(e)}")
|
||||
return 0
|
||||
|
||||
def _get_progress_data(self) -> Dict[str, int]:
|
||||
"""Get progress data for visualization."""
|
||||
try:
|
||||
progress_data = {}
|
||||
|
||||
# Count schedules by status
|
||||
for status in ScheduleStatus:
|
||||
count = self.session.query(Schedule).filter(
|
||||
Schedule.status == status
|
||||
).count()
|
||||
progress_data[status.value] = count
|
||||
|
||||
return progress_data
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error getting progress data: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _cancel_schedule(self, schedule_id: int):
|
||||
"""Cancel a schedule."""
|
||||
try:
|
||||
schedule = self.session.query(Schedule).filter(
|
||||
Schedule.id == schedule_id
|
||||
).first()
|
||||
|
||||
if schedule:
|
||||
schedule.status = ScheduleStatus.CANCELLED
|
||||
self.session.commit()
|
||||
st.success(f"Schedule {schedule_id} cancelled successfully!")
|
||||
st.experimental_rerun()
|
||||
else:
|
||||
st.error("Schedule not found.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error cancelling schedule: {str(e)}")
|
||||
self.session.rollback()
|
||||
|
||||
def _export_timeline_data(self):
|
||||
"""Export timeline data."""
|
||||
try:
|
||||
schedules = self._get_schedules_for_timeline()
|
||||
|
||||
if schedules:
|
||||
# Prepare export data
|
||||
export_data = []
|
||||
|
||||
for schedule in schedules:
|
||||
content_item = self.session.query(ContentItem).filter(
|
||||
ContentItem.id == schedule.content_item_id
|
||||
).first()
|
||||
|
||||
if content_item:
|
||||
export_data.append({
|
||||
'Schedule ID': schedule.id,
|
||||
'Title': content_item.title,
|
||||
'Content Type': content_item.content_type.value if content_item.content_type else 'Unknown',
|
||||
'Scheduled Time': schedule.scheduled_time.isoformat(),
|
||||
'Status': schedule.status.value,
|
||||
'Priority': schedule.priority,
|
||||
'Recurrence': schedule.recurrence or 'None'
|
||||
})
|
||||
|
||||
# Convert to CSV
|
||||
df = pd.DataFrame(export_data)
|
||||
csv = df.to_csv(index=False)
|
||||
|
||||
# Provide download
|
||||
st.download_button(
|
||||
label="Download Timeline Data",
|
||||
data=csv,
|
||||
file_name=f"timeline_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
|
||||
mime="text/csv"
|
||||
)
|
||||
else:
|
||||
st.warning("No data to export.")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error exporting data: {str(e)}")
|
||||
@@ -1,283 +0,0 @@
|
||||
# Platform Adapters
|
||||
|
||||
A flexible and extensible system for managing content across different social media platforms and content management systems.
|
||||
|
||||
## Overview
|
||||
|
||||
The platform adapters system provides a unified interface for publishing, managing, and analyzing content across multiple platforms. It follows a modular architecture where each platform has its own adapter implementation while maintaining a consistent interface.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Base Platform Adapter (`base.py`)**
|
||||
- Abstract base class defining the interface for all platform adapters
|
||||
- Common functionality and error handling
|
||||
- Standardized response formatting
|
||||
|
||||
2. **Platform Manager (`manager.py`)**
|
||||
- Central manager for handling multiple platform adapters
|
||||
- Platform initialization and configuration
|
||||
- Unified content publishing and management
|
||||
|
||||
3. **Unified Platform Adapter (`unified.py`)**
|
||||
- Content adaptation across different platforms
|
||||
- Platform-specific content generation
|
||||
- Performance analytics and recommendations
|
||||
|
||||
### Current Implementations
|
||||
|
||||
#### Twitter Adapter (`twitter.py`)
|
||||
- Full implementation of Twitter API integration
|
||||
- Features:
|
||||
- Tweet publishing with media support
|
||||
- Content validation
|
||||
- Analytics and engagement metrics
|
||||
- Media upload handling
|
||||
- Rate limit management
|
||||
|
||||
#### WordPress Adapter (TBD)
|
||||
- Planned implementation of WordPress REST API integration
|
||||
- Features:
|
||||
- ⏳ Post creation and management
|
||||
- ⏳ Page management
|
||||
- ⏳ Media library integration
|
||||
- ⏳ Category and tag management
|
||||
- ⏳ Custom post type support
|
||||
- ⏳ SEO metadata management
|
||||
- ⏳ Comment moderation
|
||||
- ⏳ User management
|
||||
|
||||
#### Wix Adapter (TBD)
|
||||
- Planned implementation of Wix API integration
|
||||
- Features:
|
||||
- ⏳ Blog post management
|
||||
- ⏳ Page content management
|
||||
- ⏳ Media upload and management
|
||||
- ⏳ SEO settings
|
||||
- ⏳ Collection management
|
||||
- ⏳ Form submissions handling
|
||||
- ⏳ Site settings management
|
||||
- ⏳ Analytics integration
|
||||
|
||||
## Features
|
||||
|
||||
### Core Features
|
||||
- ✅ Multi-platform content publishing
|
||||
- ✅ Content validation and optimization
|
||||
- ✅ Analytics and performance tracking
|
||||
- ✅ Media handling
|
||||
- ✅ Error handling and logging
|
||||
- ✅ Platform-specific content adaptation
|
||||
|
||||
### Platform-Specific Features
|
||||
|
||||
#### Twitter
|
||||
- ✅ Tweet publishing
|
||||
- ✅ Media attachments
|
||||
- ✅ Analytics tracking
|
||||
- ✅ Content validation
|
||||
- ✅ Rate limit handling
|
||||
|
||||
#### Instagram (TBD)
|
||||
- ⏳ Post creation
|
||||
- ⏳ Story publishing
|
||||
- ⏳ Hashtag optimization
|
||||
- ⏳ Media handling
|
||||
|
||||
#### LinkedIn (TBD)
|
||||
- ⏳ Post creation
|
||||
- ⏳ Article publishing
|
||||
- ⏳ Professional content optimization
|
||||
- ⏳ Company page integration
|
||||
|
||||
#### Facebook (TBD)
|
||||
- ⏳ Post creation
|
||||
- ⏳ Page management
|
||||
- ⏳ Audience targeting
|
||||
- ⏳ Analytics integration
|
||||
|
||||
#### WordPress (TBD)
|
||||
- ⏳ REST API integration
|
||||
- ⏳ Content synchronization
|
||||
- ⏳ Media management
|
||||
- ⏳ SEO optimization
|
||||
- ⏳ Custom post types
|
||||
- ⏳ Plugin integration
|
||||
|
||||
#### Wix (TBD)
|
||||
- ⏳ API integration
|
||||
- ⏳ Content management
|
||||
- ⏳ Media handling
|
||||
- ⏳ SEO settings
|
||||
- ⏳ Collection management
|
||||
- ⏳ Analytics integration
|
||||
|
||||
## Configuration
|
||||
|
||||
Each platform adapter requires specific configuration parameters:
|
||||
|
||||
### Twitter Configuration
|
||||
```python
|
||||
{
|
||||
'api_key': 'your_api_key',
|
||||
'api_secret': 'your_api_secret',
|
||||
'access_token': 'your_access_token',
|
||||
'access_token_secret': 'your_access_token_secret'
|
||||
}
|
||||
```
|
||||
|
||||
### WordPress Configuration
|
||||
```python
|
||||
{
|
||||
'site_url': 'https://your-wordpress-site.com',
|
||||
'username': 'your_username',
|
||||
'application_password': 'your_application_password',
|
||||
'api_version': 'v2'
|
||||
}
|
||||
```
|
||||
|
||||
### Wix Configuration
|
||||
```python
|
||||
{
|
||||
'site_id': 'your_site_id',
|
||||
'api_key': 'your_api_key',
|
||||
'access_token': 'your_access_token'
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
```python
|
||||
from lib.integrations.platform_adapters.manager import PlatformManager
|
||||
|
||||
# Initialize platform manager
|
||||
config = {
|
||||
'platforms': {
|
||||
'twitter': {
|
||||
'api_key': 'your_api_key',
|
||||
'api_secret': 'your_api_secret',
|
||||
'access_token': 'your_access_token',
|
||||
'access_token_secret': 'your_access_token_secret'
|
||||
},
|
||||
'wordpress': {
|
||||
'site_url': 'https://your-wordpress-site.com',
|
||||
'username': 'your_username',
|
||||
'application_password': 'your_application_password'
|
||||
},
|
||||
'wix': {
|
||||
'site_id': 'your_site_id',
|
||||
'api_key': 'your_api_key',
|
||||
'access_token': 'your_access_token'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manager = PlatformManager(config)
|
||||
|
||||
# Publish content
|
||||
content = {
|
||||
'text': 'Hello, World!',
|
||||
'media': [
|
||||
{
|
||||
'url': 'https://example.com/image.jpg',
|
||||
'type': 'image'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
result = await manager.publish_content(content, platforms=['twitter', 'wordpress', 'wix'])
|
||||
```
|
||||
|
||||
## TBD Features
|
||||
|
||||
### Platform Support
|
||||
- [ ] Instagram adapter implementation
|
||||
- [ ] LinkedIn adapter implementation
|
||||
- [ ] Facebook adapter implementation
|
||||
- [ ] YouTube adapter implementation
|
||||
- [ ] TikTok adapter implementation
|
||||
- [ ] WordPress adapter implementation
|
||||
- [ ] Wix adapter implementation
|
||||
|
||||
### Content Management
|
||||
- [ ] Bulk content publishing
|
||||
- [ ] Content scheduling
|
||||
- [ ] Content templates
|
||||
- [ ] A/B testing support
|
||||
- [ ] Content versioning
|
||||
- [ ] Cross-platform content synchronization
|
||||
- [ ] CMS-specific content optimization
|
||||
|
||||
### Analytics
|
||||
- [ ] Cross-platform analytics
|
||||
- [ ] Custom metric tracking
|
||||
- [ ] Automated reporting
|
||||
- [ ] Performance optimization suggestions
|
||||
- [ ] ROI tracking
|
||||
- [ ] CMS-specific analytics integration
|
||||
|
||||
### Media Handling
|
||||
- [ ] Advanced media optimization
|
||||
- [ ] Media library management
|
||||
- [ ] Automatic media resizing
|
||||
- [ ] Media format conversion
|
||||
- [ ] Media metadata management
|
||||
- [ ] Cross-platform media synchronization
|
||||
|
||||
### Security
|
||||
- [ ] OAuth2 implementation
|
||||
- [ ] API key rotation
|
||||
- [ ] Rate limit handling
|
||||
- [ ] Error recovery
|
||||
- [ ] Audit logging
|
||||
- [ ] CMS-specific security features
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Implement your changes
|
||||
4. Add tests
|
||||
5. Submit a pull request
|
||||
|
||||
## Testing
|
||||
|
||||
Each platform adapter should include:
|
||||
- Unit tests
|
||||
- Integration tests
|
||||
- Mock API responses
|
||||
- Error handling tests
|
||||
- Rate limit tests
|
||||
- CMS-specific test cases
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system implements standardized error handling:
|
||||
- Platform-specific error mapping
|
||||
- Retry mechanisms
|
||||
- Error logging
|
||||
- User-friendly error messages
|
||||
- CMS-specific error handling
|
||||
|
||||
## Logging
|
||||
|
||||
Comprehensive logging system:
|
||||
- Platform operations
|
||||
- API calls
|
||||
- Error tracking
|
||||
- Performance metrics
|
||||
- Debug information
|
||||
- CMS-specific logging
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Python 3.11+
|
||||
- tweepy (for Twitter integration)
|
||||
- requests
|
||||
- loguru
|
||||
- typing
|
||||
- datetime
|
||||
- wordpress-xmlrpc (for WordPress integration)
|
||||
- wix-api-client (for Wix integration)
|
||||
@@ -1,15 +0,0 @@
|
||||
"""
|
||||
Platform adapters for content publishing and management.
|
||||
"""
|
||||
|
||||
from .base import PlatformAdapter
|
||||
from .manager import PlatformManager
|
||||
from .twitter import TwitterAdapter
|
||||
from .unified import UnifiedPlatformAdapter
|
||||
|
||||
__all__ = [
|
||||
'PlatformAdapter',
|
||||
'PlatformManager',
|
||||
'TwitterAdapter',
|
||||
'UnifiedPlatformAdapter'
|
||||
]
|
||||
@@ -1,157 +0,0 @@
|
||||
"""
|
||||
Base platform adapter class.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class PlatformAdapter(ABC):
|
||||
"""Base class for platform-specific adapters."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""Initialize platform adapter with configuration."""
|
||||
self.config = config
|
||||
self.platform_name = self.__class__.__name__.replace('Adapter', '').upper()
|
||||
|
||||
@abstractmethod
|
||||
async def publish_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
schedule_time: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Publish content to the platform."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_content_status(
|
||||
self,
|
||||
content_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Get the status of published content."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def delete_content(
|
||||
self,
|
||||
content_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete published content."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def update_content(
|
||||
self,
|
||||
content_id: str,
|
||||
updates: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Update published content."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_analytics(
|
||||
self,
|
||||
content_id: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get analytics for published content."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def validate_content(
|
||||
self,
|
||||
content: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate content before publishing."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_optimal_publish_time(
|
||||
self,
|
||||
content_type: str,
|
||||
target_audience: Optional[Dict[str, Any]] = None
|
||||
) -> datetime:
|
||||
"""Get optimal publish time for content."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_platform_limits(
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get platform-specific limits and constraints."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_supported_content_types(
|
||||
self
|
||||
) -> List[str]:
|
||||
"""Get list of supported content types."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_platform_metrics(
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get platform-specific metrics and statistics."""
|
||||
pass
|
||||
|
||||
def _format_error_response(
|
||||
self,
|
||||
error: Exception,
|
||||
context: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Format error response."""
|
||||
return {
|
||||
'success': False,
|
||||
'platform': self.platform_name,
|
||||
'error': str(error),
|
||||
'error_type': error.__class__.__name__,
|
||||
'context': context or {}
|
||||
}
|
||||
|
||||
def _format_success_response(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
context: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Format success response."""
|
||||
return {
|
||||
'success': True,
|
||||
'platform': self.platform_name,
|
||||
'data': data,
|
||||
'context': context or {}
|
||||
}
|
||||
|
||||
def _validate_config(self) -> None:
|
||||
"""Validate platform configuration."""
|
||||
required_fields = self.get_required_config_fields()
|
||||
missing_fields = [
|
||||
field for field in required_fields
|
||||
if field not in self.config
|
||||
]
|
||||
|
||||
if missing_fields:
|
||||
raise ValueError(
|
||||
f"Missing required configuration fields: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_required_config_fields(cls) -> List[str]:
|
||||
"""Get list of required configuration fields."""
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_platform_name(cls) -> str:
|
||||
"""Get platform name."""
|
||||
return cls.__name__.replace('Adapter', '').upper()
|
||||
|
||||
@classmethod
|
||||
def get_platform_description(cls) -> str:
|
||||
"""Get platform description."""
|
||||
return "Base platform adapter"
|
||||
|
||||
@classmethod
|
||||
def get_platform_version(cls) -> str:
|
||||
"""Get platform adapter version."""
|
||||
return "1.0.0"
|
||||
@@ -1,284 +0,0 @@
|
||||
"""
|
||||
Platform manager for handling multiple platform adapters.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional, Type
|
||||
from datetime import datetime
|
||||
|
||||
from .base import PlatformAdapter
|
||||
from .twitter import TwitterAdapter
|
||||
from .wix import WixAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PlatformManager:
|
||||
"""Manages multiple platform adapters."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""Initialize platform manager with configuration."""
|
||||
self.config = config
|
||||
self.adapters: Dict[str, PlatformAdapter] = {}
|
||||
self._initialize_adapters()
|
||||
|
||||
def _initialize_adapters(self) -> None:
|
||||
"""Initialize platform adapters based on configuration."""
|
||||
platform_configs = self.config.get('platforms', {})
|
||||
|
||||
for platform, config in platform_configs.items():
|
||||
try:
|
||||
adapter = self._create_adapter(platform, config)
|
||||
if adapter:
|
||||
self.adapters[platform] = adapter
|
||||
logger.info(f"Initialized {platform} adapter")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize {platform} adapter: {str(e)}")
|
||||
|
||||
def _create_adapter(
|
||||
self,
|
||||
platform: str,
|
||||
config: Dict[str, Any]
|
||||
) -> Optional[PlatformAdapter]:
|
||||
"""Create platform adapter instance."""
|
||||
adapter_map: Dict[str, Type[PlatformAdapter]] = {
|
||||
'TWITTER': TwitterAdapter,
|
||||
'WIX': WixAdapter,
|
||||
# Add other platform adapters here
|
||||
}
|
||||
|
||||
adapter_class = adapter_map.get(platform.upper())
|
||||
if not adapter_class:
|
||||
logger.warning(f"Unsupported platform: {platform}")
|
||||
return None
|
||||
|
||||
try:
|
||||
return adapter_class(config)
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"Failed to create {platform} adapter: {str(e)}"
|
||||
)
|
||||
|
||||
async def publish_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
platforms: List[str],
|
||||
schedule_time: Optional[datetime] = None
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""Publish content to multiple platforms."""
|
||||
results = {}
|
||||
|
||||
for platform in platforms:
|
||||
if platform not in self.adapters:
|
||||
results[platform] = {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
continue
|
||||
|
||||
try:
|
||||
result = await self.adapters[platform].publish_content(
|
||||
content,
|
||||
schedule_time
|
||||
)
|
||||
results[platform] = result
|
||||
except Exception as e:
|
||||
results[platform] = {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
async def get_content_status(
|
||||
self,
|
||||
content_id: str,
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Get content status from a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].get_content_status(content_id)
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def delete_content(
|
||||
self,
|
||||
content_id: str,
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete content from a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].delete_content(content_id)
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def update_content(
|
||||
self,
|
||||
content_id: str,
|
||||
updates: Dict[str, Any],
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Update content on a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].update_content(
|
||||
content_id,
|
||||
updates
|
||||
)
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def get_analytics(
|
||||
self,
|
||||
content_id: str,
|
||||
platform: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get analytics from a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].get_analytics(
|
||||
content_id,
|
||||
start_date,
|
||||
end_date
|
||||
)
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def validate_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate content for a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].validate_content(content)
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def get_optimal_publish_time(
|
||||
self,
|
||||
content_type: str,
|
||||
platform: str,
|
||||
target_audience: Optional[Dict[str, Any]] = None
|
||||
) -> datetime:
|
||||
"""Get optimal publish time for a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
raise Exception(f"Platform adapter not found: {platform}")
|
||||
|
||||
return await self.adapters[platform].get_optimal_publish_time(
|
||||
content_type,
|
||||
target_audience
|
||||
)
|
||||
|
||||
async def get_platform_limits(
|
||||
self,
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Get platform limits for a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].get_platform_limits()
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
async def get_supported_content_types(
|
||||
self,
|
||||
platform: str
|
||||
) -> List[str]:
|
||||
"""Get supported content types for a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
raise Exception(f"Platform adapter not found: {platform}")
|
||||
|
||||
return await self.adapters[platform].get_supported_content_types()
|
||||
|
||||
async def get_platform_metrics(
|
||||
self,
|
||||
platform: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Get platform metrics for a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
try:
|
||||
return await self.adapters[platform].get_platform_metrics()
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def get_available_platforms(self) -> List[str]:
|
||||
"""Get list of available platform adapters."""
|
||||
return list(self.adapters.keys())
|
||||
|
||||
def get_platform_info(self, platform: str) -> Dict[str, Any]:
|
||||
"""Get information about a specific platform."""
|
||||
if platform not in self.adapters:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Platform adapter not found: {platform}"
|
||||
}
|
||||
|
||||
adapter = self.adapters[platform]
|
||||
return {
|
||||
'success': True,
|
||||
'name': adapter.get_platform_name(),
|
||||
'description': adapter.get_platform_description(),
|
||||
'version': adapter.get_platform_version(),
|
||||
'required_config': adapter.get_required_config_fields()
|
||||
}
|
||||
@@ -1,568 +0,0 @@
|
||||
"""
|
||||
Twitter platform adapter implementation with enhanced error handling and real metrics.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import tweepy
|
||||
from tweepy.models import Status
|
||||
import logging
|
||||
import time
|
||||
|
||||
from .base import PlatformAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TwitterAdapter(PlatformAdapter):
|
||||
"""Enhanced Twitter platform adapter with real metrics and error handling."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""Initialize Twitter adapter with configuration."""
|
||||
super().__init__(config)
|
||||
self._validate_config()
|
||||
self._initialize_client()
|
||||
self.rate_limit_tracker = {}
|
||||
|
||||
def _initialize_client(self) -> None:
|
||||
"""Initialize Twitter API client with enhanced error handling."""
|
||||
try:
|
||||
# Initialize OAuth handler
|
||||
auth = tweepy.OAuthHandler(
|
||||
self.config['api_key'],
|
||||
self.config['api_secret']
|
||||
)
|
||||
auth.set_access_token(
|
||||
self.config['access_token'],
|
||||
self.config['access_token_secret']
|
||||
)
|
||||
|
||||
# Create API client with wait_on_rate_limit
|
||||
self.client = tweepy.API(
|
||||
auth,
|
||||
wait_on_rate_limit=True,
|
||||
retry_count=3,
|
||||
retry_delay=5
|
||||
)
|
||||
|
||||
# Verify credentials
|
||||
user = self.client.verify_credentials()
|
||||
if not user:
|
||||
raise Exception("Failed to verify Twitter credentials")
|
||||
|
||||
logger.info(f"Twitter client initialized for @{user.screen_name}")
|
||||
|
||||
except tweepy.Unauthorized:
|
||||
raise Exception("Invalid Twitter API credentials")
|
||||
except tweepy.Forbidden:
|
||||
raise Exception("Access forbidden - check API permissions")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to initialize Twitter client: {str(e)}")
|
||||
|
||||
async def publish_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
schedule_time: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Publish content to Twitter with enhanced error handling."""
|
||||
try:
|
||||
# Validate content first
|
||||
validation = await self.validate_content(content)
|
||||
if not validation.get('success'):
|
||||
return validation
|
||||
|
||||
# Check rate limits
|
||||
if not self._check_rate_limit('tweets'):
|
||||
return self._format_error_response(
|
||||
Exception("Rate limit exceeded for tweets"),
|
||||
{'content': content}
|
||||
)
|
||||
|
||||
# Prepare tweet content
|
||||
tweet_text = content.get('text', '')
|
||||
media_ids = []
|
||||
|
||||
# Handle media attachments if present
|
||||
if 'media' in content and content['media']:
|
||||
for media in content['media']:
|
||||
media_id = self._upload_media(media)
|
||||
if media_id:
|
||||
media_ids.append(media_id)
|
||||
|
||||
# Create tweet
|
||||
tweet = self.client.update_status(
|
||||
status=tweet_text,
|
||||
media_ids=media_ids if media_ids else None
|
||||
)
|
||||
|
||||
# Update rate limit tracker
|
||||
self._update_rate_limit_tracker('tweets')
|
||||
|
||||
# Format response with comprehensive data
|
||||
tweet_data = {
|
||||
'id': tweet.id_str,
|
||||
'text': tweet.text,
|
||||
'created_at': tweet.created_at.isoformat(),
|
||||
'user': {
|
||||
'screen_name': tweet.user.screen_name,
|
||||
'name': tweet.user.name,
|
||||
'followers_count': tweet.user.followers_count
|
||||
},
|
||||
'metrics': {
|
||||
'retweet_count': tweet.retweet_count,
|
||||
'favorite_count': tweet.favorite_count,
|
||||
'reply_count': getattr(tweet, 'reply_count', 0)
|
||||
},
|
||||
'urls': {
|
||||
'tweet_url': f"https://twitter.com/{tweet.user.screen_name}/status/{tweet.id_str}"
|
||||
}
|
||||
}
|
||||
|
||||
return self._format_success_response(tweet_data)
|
||||
|
||||
except tweepy.Unauthorized:
|
||||
return self._format_error_response(
|
||||
Exception("Authentication failed - please reconnect your account"),
|
||||
{'content': content}
|
||||
)
|
||||
except tweepy.Forbidden as e:
|
||||
error_msg = "Access forbidden"
|
||||
if "duplicate" in str(e).lower():
|
||||
error_msg = "Duplicate tweet detected - please modify your content"
|
||||
elif "automated" in str(e).lower():
|
||||
error_msg = "Tweet appears automated - please make it more personal"
|
||||
return self._format_error_response(
|
||||
Exception(error_msg),
|
||||
{'content': content}
|
||||
)
|
||||
except tweepy.TooManyRequests:
|
||||
return self._format_error_response(
|
||||
Exception("Rate limit exceeded - please wait before posting again"),
|
||||
{'content': content}
|
||||
)
|
||||
except Exception as e:
|
||||
return self._format_error_response(e, {'content': content})
|
||||
|
||||
async def get_content_status(self, content_id: str) -> Dict[str, Any]:
|
||||
"""Get status of a tweet with real metrics."""
|
||||
try:
|
||||
tweet = self.client.get_status(
|
||||
content_id,
|
||||
include_entities=True,
|
||||
tweet_mode='extended'
|
||||
)
|
||||
|
||||
tweet_data = {
|
||||
'id': tweet.id_str,
|
||||
'text': tweet.full_text,
|
||||
'created_at': tweet.created_at.isoformat(),
|
||||
'metrics': {
|
||||
'retweet_count': tweet.retweet_count,
|
||||
'favorite_count': tweet.favorite_count,
|
||||
'reply_count': getattr(tweet, 'reply_count', 0),
|
||||
'quote_count': getattr(tweet, 'quote_count', 0)
|
||||
},
|
||||
'engagement': {
|
||||
'engagement_rate': self._calculate_engagement_rate(tweet),
|
||||
'total_engagement': tweet.retweet_count + tweet.favorite_count + getattr(tweet, 'reply_count', 0)
|
||||
},
|
||||
'user': {
|
||||
'screen_name': tweet.user.screen_name,
|
||||
'followers_count': tweet.user.followers_count
|
||||
}
|
||||
}
|
||||
|
||||
return self._format_success_response(tweet_data)
|
||||
|
||||
except tweepy.NotFound:
|
||||
return self._format_error_response(
|
||||
Exception("Tweet not found - it may have been deleted"),
|
||||
{'content_id': content_id}
|
||||
)
|
||||
except Exception as e:
|
||||
return self._format_error_response(e, {'content_id': content_id})
|
||||
|
||||
async def get_analytics(
|
||||
self,
|
||||
content_id: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get comprehensive analytics for a tweet."""
|
||||
try:
|
||||
# Get tweet details
|
||||
tweet = self.client.get_status(
|
||||
content_id,
|
||||
include_entities=True,
|
||||
tweet_mode='extended'
|
||||
)
|
||||
|
||||
# Calculate engagement metrics
|
||||
total_engagement = (
|
||||
tweet.retweet_count +
|
||||
tweet.favorite_count +
|
||||
getattr(tweet, 'reply_count', 0) +
|
||||
getattr(tweet, 'quote_count', 0)
|
||||
)
|
||||
|
||||
engagement_rate = self._calculate_engagement_rate(tweet)
|
||||
|
||||
# Get time-based metrics (if tweet is recent)
|
||||
time_metrics = self._calculate_time_metrics(tweet)
|
||||
|
||||
analytics_data = {
|
||||
'tweet_id': tweet.id_str,
|
||||
'metrics': {
|
||||
'likes': tweet.favorite_count,
|
||||
'retweets': tweet.retweet_count,
|
||||
'replies': getattr(tweet, 'reply_count', 0),
|
||||
'quotes': getattr(tweet, 'quote_count', 0),
|
||||
'total_engagement': total_engagement,
|
||||
'impressions': getattr(tweet, 'impression_count', 0) # May not be available
|
||||
},
|
||||
'engagement': {
|
||||
'engagement_rate': engagement_rate,
|
||||
'likes_rate': (tweet.favorite_count / tweet.user.followers_count * 100) if tweet.user.followers_count > 0 else 0,
|
||||
'retweets_rate': (tweet.retweet_count / tweet.user.followers_count * 100) if tweet.user.followers_count > 0 else 0
|
||||
},
|
||||
'timing': time_metrics,
|
||||
'audience': {
|
||||
'followers_at_post': tweet.user.followers_count,
|
||||
'reach_percentage': (total_engagement / tweet.user.followers_count * 100) if tweet.user.followers_count > 0 else 0
|
||||
},
|
||||
'content_analysis': {
|
||||
'character_count': len(tweet.full_text),
|
||||
'hashtag_count': len([entity for entity in tweet.entities.get('hashtags', [])]),
|
||||
'mention_count': len([entity for entity in tweet.entities.get('user_mentions', [])]),
|
||||
'url_count': len([entity for entity in tweet.entities.get('urls', [])])
|
||||
}
|
||||
}
|
||||
|
||||
return self._format_success_response(analytics_data)
|
||||
|
||||
except Exception as e:
|
||||
return self._format_error_response(e, {
|
||||
'content_id': content_id,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date
|
||||
})
|
||||
|
||||
async def validate_content(self, content: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Enhanced content validation."""
|
||||
try:
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Check text
|
||||
text = content.get('text', '')
|
||||
if not text.strip():
|
||||
errors.append("Tweet text cannot be empty")
|
||||
|
||||
# Check length
|
||||
if len(text) > 280:
|
||||
errors.append(f"Tweet text exceeds 280 characters ({len(text)}/280)")
|
||||
elif len(text) > 270:
|
||||
warnings.append("Tweet is close to character limit")
|
||||
|
||||
# Check for very short tweets
|
||||
if len(text) < 10:
|
||||
warnings.append("Very short tweets may get less engagement")
|
||||
|
||||
# Check media
|
||||
media = content.get('media', [])
|
||||
if len(media) > 4:
|
||||
errors.append("Maximum 4 media attachments allowed")
|
||||
|
||||
# Check for spam indicators
|
||||
if text.count('#') > 3:
|
||||
warnings.append("Too many hashtags may reduce engagement")
|
||||
|
||||
if text.count('@') > 5:
|
||||
warnings.append("Too many mentions may appear spammy")
|
||||
|
||||
# Check for duplicate content (basic check)
|
||||
if self._is_potential_duplicate(text):
|
||||
warnings.append("Content may be similar to recent tweets")
|
||||
|
||||
if errors:
|
||||
return self._format_error_response(
|
||||
ValueError(f"Validation failed: {'; '.join(errors)}"),
|
||||
{'content': content, 'warnings': warnings}
|
||||
)
|
||||
|
||||
validation_data = {
|
||||
'valid': True,
|
||||
'content': content,
|
||||
'warnings': warnings,
|
||||
'suggestions': self._get_content_suggestions(text)
|
||||
}
|
||||
|
||||
return self._format_success_response(validation_data)
|
||||
|
||||
except Exception as e:
|
||||
return self._format_error_response(e, {'content': content})
|
||||
|
||||
def _calculate_engagement_rate(self, tweet: Status) -> float:
|
||||
"""Calculate engagement rate for a tweet."""
|
||||
try:
|
||||
total_engagement = (
|
||||
tweet.favorite_count +
|
||||
tweet.retweet_count +
|
||||
getattr(tweet, 'reply_count', 0) +
|
||||
getattr(tweet, 'quote_count', 0)
|
||||
)
|
||||
followers = tweet.user.followers_count
|
||||
return (total_engagement / followers * 100) if followers > 0 else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def _calculate_time_metrics(self, tweet: Status) -> Dict[str, Any]:
|
||||
"""Calculate time-based metrics for a tweet."""
|
||||
try:
|
||||
now = datetime.now()
|
||||
tweet_time = tweet.created_at.replace(tzinfo=None)
|
||||
age_hours = (now - tweet_time).total_seconds() / 3600
|
||||
|
||||
# Calculate engagement velocity (engagement per hour)
|
||||
total_engagement = (
|
||||
tweet.favorite_count +
|
||||
tweet.retweet_count +
|
||||
getattr(tweet, 'reply_count', 0)
|
||||
)
|
||||
|
||||
engagement_velocity = total_engagement / max(age_hours, 1)
|
||||
|
||||
return {
|
||||
'age_hours': round(age_hours, 2),
|
||||
'engagement_velocity': round(engagement_velocity, 2),
|
||||
'peak_engagement_period': self._estimate_peak_period(tweet_time),
|
||||
'posted_at': tweet_time.isoformat()
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _estimate_peak_period(self, tweet_time: datetime) -> str:
|
||||
"""Estimate if tweet was posted during peak engagement period."""
|
||||
hour = tweet_time.hour
|
||||
|
||||
if 9 <= hour <= 10:
|
||||
return "Morning Peak (9-10 AM)"
|
||||
elif 12 <= hour <= 13:
|
||||
return "Lunch Peak (12-1 PM)"
|
||||
elif 19 <= hour <= 21:
|
||||
return "Evening Peak (7-9 PM)"
|
||||
else:
|
||||
return "Off-Peak Hours"
|
||||
|
||||
def _check_rate_limit(self, endpoint: str) -> bool:
|
||||
"""Check if we're within rate limits for an endpoint."""
|
||||
try:
|
||||
rate_limits = self.client.get_rate_limit_status()
|
||||
|
||||
endpoint_map = {
|
||||
'tweets': '/statuses/update',
|
||||
'user_timeline': '/statuses/user_timeline',
|
||||
'verify_credentials': '/account/verify_credentials'
|
||||
}
|
||||
|
||||
if endpoint in endpoint_map:
|
||||
limit_info = rate_limits['resources']['statuses'].get(endpoint_map[endpoint])
|
||||
if limit_info:
|
||||
return limit_info['remaining'] > 0
|
||||
|
||||
return True # Default to allowing if we can't check
|
||||
|
||||
except Exception:
|
||||
return True # Default to allowing if check fails
|
||||
|
||||
def _update_rate_limit_tracker(self, endpoint: str) -> None:
|
||||
"""Update internal rate limit tracker."""
|
||||
now = time.time()
|
||||
if endpoint not in self.rate_limit_tracker:
|
||||
self.rate_limit_tracker[endpoint] = []
|
||||
|
||||
# Add current request
|
||||
self.rate_limit_tracker[endpoint].append(now)
|
||||
|
||||
# Clean old requests (older than 15 minutes)
|
||||
self.rate_limit_tracker[endpoint] = [
|
||||
timestamp for timestamp in self.rate_limit_tracker[endpoint]
|
||||
if now - timestamp < 900 # 15 minutes
|
||||
]
|
||||
|
||||
def _is_potential_duplicate(self, text: str) -> bool:
|
||||
"""Basic check for potential duplicate content."""
|
||||
# This is a simplified check - in production, you'd want more sophisticated detection
|
||||
try:
|
||||
# Get recent tweets from user
|
||||
recent_tweets = self.client.user_timeline(count=20, tweet_mode='extended')
|
||||
|
||||
for tweet in recent_tweets:
|
||||
# Simple similarity check
|
||||
if self._calculate_text_similarity(text, tweet.full_text) > 0.8:
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception:
|
||||
return False # If we can't check, assume it's not a duplicate
|
||||
|
||||
def _calculate_text_similarity(self, text1: str, text2: str) -> float:
|
||||
"""Calculate simple text similarity."""
|
||||
# Simple word-based similarity
|
||||
words1 = set(text1.lower().split())
|
||||
words2 = set(text2.lower().split())
|
||||
|
||||
if not words1 or not words2:
|
||||
return 0.0
|
||||
|
||||
intersection = words1.intersection(words2)
|
||||
union = words1.union(words2)
|
||||
|
||||
return len(intersection) / len(union) if union else 0.0
|
||||
|
||||
def _get_content_suggestions(self, text: str) -> List[str]:
|
||||
"""Get suggestions for improving tweet content."""
|
||||
suggestions = []
|
||||
|
||||
if len(text) < 50:
|
||||
suggestions.append("Consider adding more context to increase engagement")
|
||||
|
||||
if not any(char in text for char in '!?'):
|
||||
suggestions.append("Adding punctuation can make tweets more engaging")
|
||||
|
||||
if '#' not in text:
|
||||
suggestions.append("Consider adding 1-2 relevant hashtags")
|
||||
|
||||
if not any(emoji_char in text for emoji_char in '😀😃😄😁😆😅😂🤣'):
|
||||
suggestions.append("Emojis can increase engagement and visual appeal")
|
||||
|
||||
return suggestions
|
||||
|
||||
async def delete_content(
|
||||
self,
|
||||
content_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a tweet."""
|
||||
try:
|
||||
self.client.destroy_status(content_id)
|
||||
return self._format_success_response({
|
||||
'id': content_id,
|
||||
'deleted': True
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{'content_id': content_id}
|
||||
)
|
||||
|
||||
async def update_content(
|
||||
self,
|
||||
content_id: str,
|
||||
updates: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Update a tweet."""
|
||||
try:
|
||||
# Twitter doesn't support updating tweets
|
||||
# We'll delete the old one and create a new one
|
||||
await self.delete_content(content_id)
|
||||
return await self.publish_content(updates)
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{
|
||||
'content_id': content_id,
|
||||
'updates': updates
|
||||
}
|
||||
)
|
||||
|
||||
async def get_optimal_publish_time(
|
||||
self,
|
||||
content_type: str,
|
||||
target_audience: Optional[Dict[str, Any]] = None
|
||||
) -> datetime:
|
||||
"""Get optimal publish time for content."""
|
||||
# Implement optimal time calculation based on:
|
||||
# - Content type
|
||||
# - Target audience timezone
|
||||
# - Historical engagement data
|
||||
# For now, return current time
|
||||
return datetime.now()
|
||||
|
||||
async def get_platform_limits(
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get Twitter platform limits."""
|
||||
return self._format_success_response({
|
||||
'tweet_length': 280,
|
||||
'media_attachments': 4,
|
||||
'poll_options': 4,
|
||||
'poll_duration': 10080, # 7 days in minutes
|
||||
'rate_limits': {
|
||||
'tweets_per_day': 2000,
|
||||
'tweets_per_hour': 100
|
||||
}
|
||||
})
|
||||
|
||||
async def get_supported_content_types(
|
||||
self
|
||||
) -> List[str]:
|
||||
"""Get list of supported content types."""
|
||||
return ['TWEET', 'THREAD', 'POLL']
|
||||
|
||||
async def get_platform_metrics(
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get Twitter platform metrics."""
|
||||
try:
|
||||
account = self.client.verify_credentials()
|
||||
return self._format_success_response({
|
||||
'followers_count': account.followers_count,
|
||||
'following_count': account.friends_count,
|
||||
'tweets_count': account.statuses_count,
|
||||
'account_created_at': account.created_at.isoformat()
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(e)
|
||||
|
||||
def _upload_media(self, media: Dict[str, Any]) -> Optional[str]:
|
||||
"""Upload media to Twitter."""
|
||||
try:
|
||||
if 'url' in media:
|
||||
# Download media from URL
|
||||
response = requests.get(media['url'])
|
||||
media_file = BytesIO(response.content)
|
||||
elif 'file' in media:
|
||||
# Use local file
|
||||
media_file = open(media['file'], 'rb')
|
||||
else:
|
||||
return None
|
||||
|
||||
# Upload media
|
||||
media_upload = self.client.media_upload(
|
||||
filename=media.get('filename', 'media'),
|
||||
file=media_file
|
||||
)
|
||||
return media_upload.media_id_string
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload media: {str(e)}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_required_config_fields(cls) -> List[str]:
|
||||
"""Get list of required configuration fields."""
|
||||
return [
|
||||
'api_key',
|
||||
'api_secret',
|
||||
'access_token',
|
||||
'access_token_secret'
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_platform_description(cls) -> str:
|
||||
"""Get platform description."""
|
||||
return "Twitter platform adapter for posting and managing tweets"
|
||||
|
||||
@classmethod
|
||||
def get_platform_version(cls) -> str:
|
||||
"""Get platform adapter version."""
|
||||
return "1.0.0"
|
||||
@@ -1,290 +0,0 @@
|
||||
"""
|
||||
Unified platform adapter for content adaptation across different platforms.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
from lib.utils.website_analyzer.analyzer import WebsiteAnalyzer
|
||||
from lib.ai_seo_tools.content_gap_analysis.main import ContentGapAnalysis
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
|
||||
class UnifiedPlatformAdapter:
|
||||
"""Unified adapter for different social media platforms."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the platform adapter."""
|
||||
self.platform_handlers = {
|
||||
'instagram': self._handle_instagram,
|
||||
'linkedin': self._handle_linkedin,
|
||||
'twitter': self._handle_twitter,
|
||||
'facebook': self._handle_facebook
|
||||
}
|
||||
logger.info("UnifiedPlatformAdapter initialized")
|
||||
|
||||
def generate_content(self, platform: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate content for a specific platform.
|
||||
|
||||
Args:
|
||||
platform: Target platform
|
||||
data: Content data
|
||||
|
||||
Returns:
|
||||
Dictionary containing generated content
|
||||
"""
|
||||
try:
|
||||
handler = self.platform_handlers.get(platform.lower())
|
||||
if not handler:
|
||||
raise ValueError(f"Unsupported platform: {platform}")
|
||||
|
||||
return handler(data)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error generating content for {platform}: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'content': None
|
||||
}
|
||||
|
||||
def get_content_performance(self, content_item: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get performance metrics for content across platforms."""
|
||||
try:
|
||||
logger.info(f"Getting performance metrics for content: {content_item.get('title', 'Untitled')}")
|
||||
|
||||
# Get platform from content item
|
||||
platform = content_item.get('platforms', ['Unknown'])[0]
|
||||
|
||||
# Initialize performance metrics
|
||||
performance = {
|
||||
'engagement_metrics': {
|
||||
'likes': 0,
|
||||
'comments': 0,
|
||||
'shares': 0,
|
||||
'reach': 0
|
||||
},
|
||||
'seo_metrics': {
|
||||
'impressions': 0,
|
||||
'clicks': 0,
|
||||
'ctr': 0,
|
||||
'position': 0
|
||||
},
|
||||
'conversion_metrics': {
|
||||
'conversions': 0,
|
||||
'conversion_rate': 0,
|
||||
'revenue': 0
|
||||
},
|
||||
'platform_specific': {},
|
||||
'performance_trends': [],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
# Add platform-specific metrics
|
||||
if platform == 'WEBSITE':
|
||||
performance['platform_specific'] = {
|
||||
'bounce_rate': 0,
|
||||
'time_on_page': 0,
|
||||
'page_views': 0
|
||||
}
|
||||
|
||||
return performance
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error getting content performance: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {
|
||||
'error': error_msg,
|
||||
'metrics': {},
|
||||
'trends': {},
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
def _handle_instagram(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Instagram content generation."""
|
||||
try:
|
||||
# Generate Instagram-specific content
|
||||
caption = metadesc_generator_main(data)
|
||||
hashtags = self._generate_hashtags(data)
|
||||
|
||||
return {
|
||||
'platform': 'instagram',
|
||||
'content': {
|
||||
'caption': caption,
|
||||
'hashtags': hashtags,
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Instagram content: {str(e)}")
|
||||
return {
|
||||
'platform': 'instagram',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _handle_linkedin(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle LinkedIn content generation."""
|
||||
try:
|
||||
# Generate LinkedIn-specific content
|
||||
post = metadesc_generator_main(data)
|
||||
|
||||
return {
|
||||
'platform': 'linkedin',
|
||||
'content': {
|
||||
'post': post,
|
||||
'engagement_optimization': self._get_engagement_suggestions(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating LinkedIn content: {str(e)}")
|
||||
return {
|
||||
'platform': 'linkedin',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _handle_twitter(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Twitter content generation."""
|
||||
try:
|
||||
# Generate Twitter-specific content
|
||||
tweet = metadesc_generator_main(data)
|
||||
hashtags = self._generate_hashtags(data)
|
||||
|
||||
return {
|
||||
'platform': 'twitter',
|
||||
'content': {
|
||||
'tweet': tweet,
|
||||
'hashtags': hashtags,
|
||||
'thread_structure': self._get_thread_structure(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Twitter content: {str(e)}")
|
||||
return {
|
||||
'platform': 'twitter',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _handle_facebook(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle Facebook content generation."""
|
||||
try:
|
||||
# Generate Facebook-specific content
|
||||
post = metadesc_generator_main(data)
|
||||
|
||||
return {
|
||||
'platform': 'facebook',
|
||||
'content': {
|
||||
'post': post,
|
||||
'engagement_optimization': self._get_engagement_suggestions(data),
|
||||
'media_suggestions': self._get_media_suggestions(data)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating Facebook content: {str(e)}")
|
||||
return {
|
||||
'platform': 'facebook',
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _generate_hashtags(self, data: Dict[str, Any]) -> List[str]:
|
||||
"""Generate relevant hashtags for content."""
|
||||
try:
|
||||
# Extract keywords from content
|
||||
keywords = data.get('keywords', [])
|
||||
|
||||
# Add platform-specific hashtags
|
||||
platform = data.get('platform', '').lower()
|
||||
platform_hashtags = {
|
||||
'instagram': ['#instagood', '#photooftheday'],
|
||||
'twitter': ['#trending', '#followme'],
|
||||
'linkedin': ['#business', '#professional'],
|
||||
'facebook': ['#social', '#community']
|
||||
}.get(platform, [])
|
||||
|
||||
return keywords + platform_hashtags
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating hashtags: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_media_suggestions(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Get media suggestions for content."""
|
||||
try:
|
||||
# Generate media suggestions based on content type
|
||||
content_type = data.get('type', 'post')
|
||||
|
||||
suggestions = []
|
||||
if content_type == 'blog':
|
||||
suggestions.append({
|
||||
'type': 'featured_image',
|
||||
'description': 'Main blog post image',
|
||||
'dimensions': '1200x630'
|
||||
})
|
||||
elif content_type == 'social':
|
||||
suggestions.append({
|
||||
'type': 'post_image',
|
||||
'description': 'Social media post image',
|
||||
'dimensions': '1080x1080'
|
||||
})
|
||||
|
||||
return suggestions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting media suggestions: {str(e)}")
|
||||
return []
|
||||
|
||||
def _get_engagement_suggestions(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get engagement optimization suggestions."""
|
||||
try:
|
||||
return {
|
||||
'best_posting_times': ['9:00 AM', '5:00 PM'],
|
||||
'engagement_tips': [
|
||||
'Ask questions to encourage comments',
|
||||
'Use relevant hashtags',
|
||||
'Include a clear call-to-action'
|
||||
],
|
||||
'content_length': {
|
||||
'optimal': '150-200 characters',
|
||||
'maximum': '300 characters'
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting engagement suggestions: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _get_thread_structure(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Get thread structure for Twitter threads."""
|
||||
try:
|
||||
content = data.get('content', '')
|
||||
sentences = content.split('.')
|
||||
|
||||
thread = []
|
||||
current_tweet = ''
|
||||
|
||||
for sentence in sentences:
|
||||
if len(current_tweet + sentence) <= 280:
|
||||
current_tweet += sentence + '.'
|
||||
else:
|
||||
if current_tweet:
|
||||
thread.append({
|
||||
'content': current_tweet.strip(),
|
||||
'type': 'tweet'
|
||||
})
|
||||
current_tweet = sentence + '.'
|
||||
|
||||
if current_tweet:
|
||||
thread.append({
|
||||
'content': current_tweet.strip(),
|
||||
'type': 'tweet'
|
||||
})
|
||||
|
||||
return thread
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating thread structure: {str(e)}")
|
||||
return []
|
||||
@@ -1,327 +0,0 @@
|
||||
"""
|
||||
Wix platform adapter implementation.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from .base import PlatformAdapter
|
||||
from lib.integrations.wix.wix_api_client import WixAPIClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WixAdapter(PlatformAdapter):
|
||||
"""Wix platform adapter."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
"""Initialize Wix adapter with configuration."""
|
||||
super().__init__(config)
|
||||
self._validate_config()
|
||||
self._initialize_client()
|
||||
|
||||
def _initialize_client(self) -> None:
|
||||
"""Initialize Wix API client."""
|
||||
try:
|
||||
self.client = WixAPIClient(
|
||||
api_key=self.config.get('api_key'),
|
||||
refresh_token=self.config.get('refresh_token'),
|
||||
site_id=self.config.get('site_id')
|
||||
)
|
||||
logger.info("Successfully initialized Wix API client")
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to initialize Wix client: {str(e)}")
|
||||
|
||||
async def publish_content(
|
||||
self,
|
||||
content: Dict[str, Any],
|
||||
schedule_time: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Publish content to Wix blog."""
|
||||
try:
|
||||
# Validate content
|
||||
validation = await self.validate_content(content)
|
||||
if not validation.get('success'):
|
||||
return validation
|
||||
|
||||
# Prepare blog post data
|
||||
post_data = {
|
||||
'title': content.get('title', ''),
|
||||
'content': content.get('content', ''),
|
||||
'excerpt': content.get('excerpt', ''),
|
||||
'slug': content.get('slug', ''),
|
||||
'tags': content.get('tags', []),
|
||||
'categories': content.get('categories', []),
|
||||
'seo': content.get('seo', {}),
|
||||
'publish_date': schedule_time.isoformat() if schedule_time else None
|
||||
}
|
||||
|
||||
# Handle media attachments
|
||||
media_ids = []
|
||||
if 'media' in content:
|
||||
for media in content['media']:
|
||||
media_id = await self._upload_media(media)
|
||||
if media_id:
|
||||
media_ids.append(media_id)
|
||||
|
||||
# Create blog post
|
||||
post = self.client.create_post(post_data)
|
||||
|
||||
# Add media to post if any
|
||||
if media_ids:
|
||||
self.client.add_media_to_post(post['id'], media_ids)
|
||||
|
||||
return self._format_success_response({
|
||||
'id': post['id'],
|
||||
'title': post['title'],
|
||||
'url': post['url'],
|
||||
'created_at': post['created_at']
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{'content': content, 'schedule_time': schedule_time}
|
||||
)
|
||||
|
||||
async def get_content_status(
|
||||
self,
|
||||
content_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Get status of a blog post."""
|
||||
try:
|
||||
post = self.client.get_post(content_id)
|
||||
return self._format_success_response({
|
||||
'id': post['id'],
|
||||
'title': post['title'],
|
||||
'status': post['status'],
|
||||
'url': post['url'],
|
||||
'created_at': post['created_at'],
|
||||
'updated_at': post['updated_at'],
|
||||
'published_at': post.get('published_at')
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{'content_id': content_id}
|
||||
)
|
||||
|
||||
async def delete_content(
|
||||
self,
|
||||
content_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a blog post."""
|
||||
try:
|
||||
self.client.delete_post(content_id)
|
||||
return self._format_success_response({
|
||||
'id': content_id,
|
||||
'deleted': True
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{'content_id': content_id}
|
||||
)
|
||||
|
||||
async def update_content(
|
||||
self,
|
||||
content_id: str,
|
||||
updates: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Update a blog post."""
|
||||
try:
|
||||
post = self.client.update_post(content_id, updates)
|
||||
return self._format_success_response({
|
||||
'id': post['id'],
|
||||
'title': post['title'],
|
||||
'url': post['url'],
|
||||
'updated_at': post['updated_at']
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{
|
||||
'content_id': content_id,
|
||||
'updates': updates
|
||||
}
|
||||
)
|
||||
|
||||
async def get_analytics(
|
||||
self,
|
||||
content_id: str,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Get analytics for a blog post."""
|
||||
try:
|
||||
analytics = self.client.get_post_analytics(
|
||||
content_id,
|
||||
start_date,
|
||||
end_date
|
||||
)
|
||||
return self._format_success_response({
|
||||
'id': content_id,
|
||||
'metrics': {
|
||||
'views': analytics.get('views', 0),
|
||||
'unique_visitors': analytics.get('unique_visitors', 0),
|
||||
'average_time_on_page': analytics.get('average_time_on_page', 0),
|
||||
'bounce_rate': analytics.get('bounce_rate', 0)
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{
|
||||
'content_id': content_id,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date
|
||||
}
|
||||
)
|
||||
|
||||
async def validate_content(
|
||||
self,
|
||||
content: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate content before publishing."""
|
||||
try:
|
||||
# Check required fields
|
||||
required_fields = ['title', 'content']
|
||||
missing_fields = [
|
||||
field for field in required_fields
|
||||
if field not in content
|
||||
]
|
||||
|
||||
if missing_fields:
|
||||
return self._format_error_response(
|
||||
ValueError(f"Missing required fields: {', '.join(missing_fields)}"),
|
||||
{'content': content}
|
||||
)
|
||||
|
||||
# Check content length
|
||||
if len(content['content']) > 100000: # Wix limit
|
||||
return self._format_error_response(
|
||||
ValueError("Content exceeds maximum length of 100,000 characters"),
|
||||
{'content': content}
|
||||
)
|
||||
|
||||
# Check media attachments
|
||||
media = content.get('media', [])
|
||||
if len(media) > 20: # Wix limit
|
||||
return self._format_error_response(
|
||||
ValueError("Maximum 20 media attachments allowed"),
|
||||
{'content': content}
|
||||
)
|
||||
|
||||
return self._format_success_response({
|
||||
'valid': True,
|
||||
'content': content
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return self._format_error_response(
|
||||
e,
|
||||
{'content': content}
|
||||
)
|
||||
|
||||
async def get_optimal_publish_time(
|
||||
self,
|
||||
content_type: str,
|
||||
target_audience: Optional[Dict[str, Any]] = None
|
||||
) -> datetime:
|
||||
"""Get optimal publish time for content."""
|
||||
# Implement optimal time calculation based on:
|
||||
# - Content type
|
||||
# - Target audience timezone
|
||||
# - Historical engagement data
|
||||
# For now, return current time
|
||||
return datetime.now()
|
||||
|
||||
async def get_platform_limits(
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get Wix platform limits."""
|
||||
return self._format_success_response({
|
||||
'content_length': 100000,
|
||||
'media_attachments': 20,
|
||||
'tags_per_post': 50,
|
||||
'categories_per_post': 10,
|
||||
'rate_limits': {
|
||||
'posts_per_day': 100,
|
||||
'media_uploads_per_day': 1000
|
||||
}
|
||||
})
|
||||
|
||||
async def get_supported_content_types(
|
||||
self
|
||||
) -> List[str]:
|
||||
"""Get list of supported content types."""
|
||||
return ['BLOG_POST', 'PAGE', 'COLLECTION_ITEM']
|
||||
|
||||
async def get_platform_metrics(
|
||||
self
|
||||
) -> Dict[str, Any]:
|
||||
"""Get Wix platform metrics."""
|
||||
try:
|
||||
site_stats = self.client.get_site_statistics()
|
||||
return self._format_success_response({
|
||||
'total_posts': site_stats.get('total_posts', 0),
|
||||
'total_views': site_stats.get('total_views', 0),
|
||||
'total_comments': site_stats.get('total_comments', 0),
|
||||
'average_engagement': site_stats.get('average_engagement', 0)
|
||||
})
|
||||
except Exception as e:
|
||||
return self._format_error_response(e)
|
||||
|
||||
async def _upload_media(
|
||||
self,
|
||||
media: Dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""Upload media to Wix."""
|
||||
try:
|
||||
if 'url' in media:
|
||||
# Download media from URL
|
||||
response = requests.get(media['url'])
|
||||
media_file = BytesIO(response.content)
|
||||
filename = media.get('filename', 'media')
|
||||
elif 'file' in media:
|
||||
# Use local file
|
||||
file_path = Path(media['file'])
|
||||
media_file = open(file_path, 'rb')
|
||||
filename = file_path.name
|
||||
else:
|
||||
return None
|
||||
|
||||
# Upload media
|
||||
media_id = self.client.upload_media(
|
||||
file=media_file,
|
||||
filename=filename,
|
||||
mime_type=media.get('mime_type')
|
||||
)
|
||||
return media_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload media: {str(e)}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_required_config_fields(cls) -> List[str]:
|
||||
"""Get list of required configuration fields."""
|
||||
return [
|
||||
'api_key',
|
||||
'refresh_token',
|
||||
'site_id'
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_platform_description(cls) -> str:
|
||||
"""Get platform description."""
|
||||
return "Wix platform adapter for managing blog posts and content"
|
||||
|
||||
@classmethod
|
||||
def get_platform_version(cls) -> str:
|
||||
"""Get platform adapter version."""
|
||||
return "1.0.0"
|
||||
@@ -1,337 +0,0 @@
|
||||
"""
|
||||
Twitter Authentication Bridge
|
||||
Connects the platform adapter with the UI authentication system for secure Twitter integration.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import tweepy
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import base64
|
||||
from cryptography.fernet import Fernet
|
||||
import logging
|
||||
|
||||
from .platform_adapters.twitter import TwitterAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TwitterAuthBridge:
|
||||
"""Bridge between Twitter authentication and platform adapter."""
|
||||
|
||||
def __init__(self):
|
||||
self.config_dir = Path("config/twitter")
|
||||
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.encryption_key = self._get_or_create_encryption_key()
|
||||
|
||||
def _get_or_create_encryption_key(self) -> bytes:
|
||||
"""Get or create encryption key for secure credential storage."""
|
||||
key_file = self.config_dir / "encryption.key"
|
||||
|
||||
if key_file.exists():
|
||||
with open(key_file, 'rb') as f:
|
||||
return f.read()
|
||||
else:
|
||||
key = Fernet.generate_key()
|
||||
with open(key_file, 'wb') as f:
|
||||
f.write(key)
|
||||
return key
|
||||
|
||||
def encrypt_credentials(self, credentials: Dict[str, str]) -> str:
|
||||
"""Encrypt Twitter credentials for secure storage."""
|
||||
try:
|
||||
fernet = Fernet(self.encryption_key)
|
||||
credentials_json = json.dumps(credentials)
|
||||
encrypted_data = fernet.encrypt(credentials_json.encode())
|
||||
return base64.b64encode(encrypted_data).decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to encrypt credentials: {str(e)}")
|
||||
raise
|
||||
|
||||
def decrypt_credentials(self, encrypted_data: str) -> Dict[str, str]:
|
||||
"""Decrypt Twitter credentials from secure storage."""
|
||||
try:
|
||||
fernet = Fernet(self.encryption_key)
|
||||
encrypted_bytes = base64.b64decode(encrypted_data.encode())
|
||||
decrypted_data = fernet.decrypt(encrypted_bytes)
|
||||
return json.loads(decrypted_data.decode())
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt credentials: {str(e)}")
|
||||
raise
|
||||
|
||||
def save_credentials(self, user_id: str, credentials: Dict[str, str]) -> bool:
|
||||
"""Save encrypted Twitter credentials to file."""
|
||||
try:
|
||||
# Create user-specific credentials file
|
||||
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:16]
|
||||
creds_file = self.config_dir / f"user_{user_hash}.enc"
|
||||
|
||||
# Add timestamp and validation
|
||||
credentials_with_meta = {
|
||||
**credentials,
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'user_id_hash': user_hash
|
||||
}
|
||||
|
||||
# Encrypt and save
|
||||
encrypted_data = self.encrypt_credentials(credentials_with_meta)
|
||||
with open(creds_file, 'w') as f:
|
||||
f.write(encrypted_data)
|
||||
|
||||
logger.info(f"Credentials saved for user {user_hash}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save credentials: {str(e)}")
|
||||
return False
|
||||
|
||||
def load_credentials(self, user_id: str) -> Optional[Dict[str, str]]:
|
||||
"""Load and decrypt Twitter credentials from file."""
|
||||
try:
|
||||
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:16]
|
||||
creds_file = self.config_dir / f"user_{user_hash}.enc"
|
||||
|
||||
if not creds_file.exists():
|
||||
logger.warning(f"No credentials found for user {user_hash}")
|
||||
return None
|
||||
|
||||
# Load and decrypt
|
||||
with open(creds_file, 'r') as f:
|
||||
encrypted_data = f.read()
|
||||
|
||||
credentials = self.decrypt_credentials(encrypted_data)
|
||||
|
||||
# Validate credentials are not expired (optional)
|
||||
created_at = datetime.fromisoformat(credentials.get('created_at', ''))
|
||||
if datetime.now() - created_at > timedelta(days=365): # 1 year expiry
|
||||
logger.warning(f"Credentials expired for user {user_hash}")
|
||||
return None
|
||||
|
||||
# Remove metadata before returning
|
||||
clean_credentials = {k: v for k, v in credentials.items()
|
||||
if k not in ['created_at', 'user_id_hash']}
|
||||
|
||||
return clean_credentials
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load credentials: {str(e)}")
|
||||
return None
|
||||
|
||||
def delete_credentials(self, user_id: str) -> bool:
|
||||
"""Delete stored Twitter credentials."""
|
||||
try:
|
||||
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:16]
|
||||
creds_file = self.config_dir / f"user_{user_hash}.enc"
|
||||
|
||||
if creds_file.exists():
|
||||
creds_file.unlink()
|
||||
logger.info(f"Credentials deleted for user {user_hash}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete credentials: {str(e)}")
|
||||
return False
|
||||
|
||||
def validate_credentials(self, credentials: Dict[str, str]) -> Tuple[bool, str]:
|
||||
"""Validate Twitter API credentials."""
|
||||
try:
|
||||
# Check required fields
|
||||
required_fields = ['api_key', 'api_secret', 'access_token', 'access_token_secret']
|
||||
missing_fields = [field for field in required_fields if not credentials.get(field)]
|
||||
|
||||
if missing_fields:
|
||||
return False, f"Missing required fields: {', '.join(missing_fields)}"
|
||||
|
||||
# Test connection
|
||||
auth = tweepy.OAuthHandler(
|
||||
credentials['api_key'],
|
||||
credentials['api_secret']
|
||||
)
|
||||
auth.set_access_token(
|
||||
credentials['access_token'],
|
||||
credentials['access_token_secret']
|
||||
)
|
||||
|
||||
api = tweepy.API(auth)
|
||||
user = api.verify_credentials()
|
||||
|
||||
if user:
|
||||
return True, f"Valid credentials for @{user.screen_name}"
|
||||
else:
|
||||
return False, "Failed to verify credentials"
|
||||
|
||||
except tweepy.Unauthorized:
|
||||
return False, "Invalid API credentials"
|
||||
except tweepy.Forbidden:
|
||||
return False, "Access forbidden - check API permissions"
|
||||
except tweepy.TooManyRequests:
|
||||
return False, "Rate limit exceeded - try again later"
|
||||
except Exception as e:
|
||||
return False, f"Connection error: {str(e)}"
|
||||
|
||||
def get_twitter_adapter(self, user_id: str) -> Optional[TwitterAdapter]:
|
||||
"""Get configured Twitter adapter for user."""
|
||||
try:
|
||||
# First check session state
|
||||
if 'twitter_adapter' in st.session_state:
|
||||
return st.session_state.twitter_adapter
|
||||
|
||||
# Load credentials
|
||||
credentials = self.load_credentials(user_id)
|
||||
if not credentials:
|
||||
return None
|
||||
|
||||
# Validate credentials
|
||||
is_valid, message = self.validate_credentials(credentials)
|
||||
if not is_valid:
|
||||
logger.error(f"Invalid credentials: {message}")
|
||||
return None
|
||||
|
||||
# Create adapter
|
||||
adapter = TwitterAdapter(credentials)
|
||||
|
||||
# Cache in session state
|
||||
st.session_state.twitter_adapter = adapter
|
||||
|
||||
return adapter
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get Twitter adapter: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_user_info(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get Twitter user information."""
|
||||
try:
|
||||
adapter = self.get_twitter_adapter(user_id)
|
||||
if not adapter:
|
||||
return None
|
||||
|
||||
# Get user info from Twitter
|
||||
user = adapter.client.verify_credentials()
|
||||
|
||||
user_info = {
|
||||
'id': user.id_str,
|
||||
'screen_name': user.screen_name,
|
||||
'name': user.name,
|
||||
'description': user.description,
|
||||
'followers_count': user.followers_count,
|
||||
'friends_count': user.friends_count,
|
||||
'statuses_count': user.statuses_count,
|
||||
'profile_image_url': user.profile_image_url_https,
|
||||
'profile_banner_url': getattr(user, 'profile_banner_url', ''),
|
||||
'verified': user.verified,
|
||||
'created_at': user.created_at.isoformat(),
|
||||
'location': user.location or '',
|
||||
'url': user.url or ''
|
||||
}
|
||||
|
||||
return user_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user info: {str(e)}")
|
||||
return None
|
||||
|
||||
def setup_session_state(self, user_id: str) -> bool:
|
||||
"""Setup session state with Twitter authentication."""
|
||||
try:
|
||||
# Load credentials
|
||||
credentials = self.load_credentials(user_id)
|
||||
if not credentials:
|
||||
return False
|
||||
|
||||
# Get user info
|
||||
user_info = self.get_user_info(user_id)
|
||||
if not user_info:
|
||||
return False
|
||||
|
||||
# Setup session state
|
||||
st.session_state.twitter_authenticated = True
|
||||
st.session_state.twitter_user_id = user_id
|
||||
st.session_state.twitter_user_info = user_info
|
||||
st.session_state.twitter_config = credentials
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to setup session state: {str(e)}")
|
||||
return False
|
||||
|
||||
def clear_session_state(self) -> None:
|
||||
"""Clear Twitter authentication from session state."""
|
||||
keys_to_clear = [
|
||||
'twitter_authenticated',
|
||||
'twitter_user_id',
|
||||
'twitter_user_info',
|
||||
'twitter_config',
|
||||
'twitter_adapter'
|
||||
]
|
||||
|
||||
for key in keys_to_clear:
|
||||
if key in st.session_state:
|
||||
del st.session_state[key]
|
||||
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Check if user is authenticated with Twitter."""
|
||||
return (
|
||||
st.session_state.get('twitter_authenticated', False) and
|
||||
st.session_state.get('twitter_user_info') is not None and
|
||||
st.session_state.get('twitter_config') is not None
|
||||
)
|
||||
|
||||
def get_rate_limit_status(self, user_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get current rate limit status."""
|
||||
try:
|
||||
adapter = self.get_twitter_adapter(user_id)
|
||||
if not adapter:
|
||||
return None
|
||||
|
||||
rate_limits = adapter.client.get_rate_limit_status()
|
||||
|
||||
# Extract relevant rate limits
|
||||
relevant_limits = {
|
||||
'tweets': rate_limits['resources']['statuses']['/statuses/update'],
|
||||
'user_timeline': rate_limits['resources']['statuses']['/statuses/user_timeline'],
|
||||
'verify_credentials': rate_limits['resources']['account']['/account/verify_credentials']
|
||||
}
|
||||
|
||||
return relevant_limits
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get rate limit status: {str(e)}")
|
||||
return None
|
||||
|
||||
# Global instance
|
||||
twitter_auth = TwitterAuthBridge()
|
||||
|
||||
# Convenience functions for UI
|
||||
def save_twitter_credentials(user_id: str, credentials: Dict[str, str]) -> bool:
|
||||
"""Save Twitter credentials (convenience function)."""
|
||||
return twitter_auth.save_credentials(user_id, credentials)
|
||||
|
||||
def load_twitter_credentials(user_id: str) -> Optional[Dict[str, str]]:
|
||||
"""Load Twitter credentials (convenience function)."""
|
||||
return twitter_auth.load_credentials(user_id)
|
||||
|
||||
def get_twitter_adapter(user_id: str) -> Optional[TwitterAdapter]:
|
||||
"""Get Twitter adapter (convenience function)."""
|
||||
return twitter_auth.get_twitter_adapter(user_id)
|
||||
|
||||
def is_twitter_authenticated() -> bool:
|
||||
"""Check if Twitter is authenticated (convenience function)."""
|
||||
return twitter_auth.is_authenticated()
|
||||
|
||||
def setup_twitter_session(user_id: str) -> bool:
|
||||
"""Setup Twitter session (convenience function)."""
|
||||
return twitter_auth.setup_session_state(user_id)
|
||||
|
||||
def clear_twitter_session() -> None:
|
||||
"""Clear Twitter session (convenience function)."""
|
||||
twitter_auth.clear_session_state()
|
||||
|
||||
def validate_twitter_credentials(credentials: Dict[str, str]) -> Tuple[bool, str]:
|
||||
"""Validate Twitter credentials (convenience function)."""
|
||||
return twitter_auth.validate_credentials(credentials)
|
||||
@@ -1,208 +0,0 @@
|
||||
# Wix Blog Integration for Alwrity
|
||||
|
||||
This integration allows you to publish blog content from Alwrity directly to your Wix site using the Wix REST API.
|
||||
|
||||
## Features
|
||||
|
||||
- **Blog Post Management**: Create, update, and delete blog posts
|
||||
- **Media Management**: Upload images and other media files
|
||||
- **SEO Optimization**: Comprehensive SEO settings and analysis
|
||||
- **Category Management**: Create and manage blog categories
|
||||
- **Markdown Support**: Write in markdown and publish as HTML
|
||||
- **Streamlit UI**: User-friendly interface for publishing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using this integration, you'll need:
|
||||
|
||||
1. A Wix site with the Blog feature enabled
|
||||
2. Wix API credentials (refresh token and site ID)
|
||||
3. Python 3.7+ with required dependencies
|
||||
|
||||
## Getting Wix API Credentials
|
||||
|
||||
To use this integration, you need to obtain a refresh token and site ID from Wix:
|
||||
|
||||
1. **Create a Wix Developer Account**:
|
||||
- Go to [Wix Developers](https://dev.wix.com/) and sign up or log in
|
||||
- Create a new OAuth app
|
||||
|
||||
2. **Configure OAuth App**:
|
||||
- Set a name and description for your app
|
||||
- Add redirect URLs (e.g., `https://localhost:3000/oauth/callback`)
|
||||
- Save the app and note the App ID and App Secret
|
||||
|
||||
3. **Get a Refresh Token**:
|
||||
- Follow the OAuth flow to get an authorization code
|
||||
- Exchange the code for an access token and refresh token
|
||||
- Detailed instructions: [Wix OAuth Documentation](https://dev.wix.com/api/rest/getting-started/authentication)
|
||||
|
||||
4. **Get Your Site ID**:
|
||||
- Log in to your Wix account
|
||||
- Go to your site's dashboard
|
||||
- The site ID is in the URL: `https://manage.wix.com/dashboard/{SITE_ID}/home`
|
||||
|
||||
## Installation
|
||||
|
||||
The Wix integration is included with Alwrity. No additional installation is required.
|
||||
|
||||
## Usage
|
||||
|
||||
### Using the Streamlit UI
|
||||
|
||||
1. Navigate to the Wix integration in the Alwrity UI
|
||||
2. Enter your Wix refresh token and site ID
|
||||
3. Fill in the blog details and content
|
||||
4. Click "Publish to Wix"
|
||||
|
||||
### Using the Python API
|
||||
|
||||
```python
|
||||
from lib.integrations.wix_integration import WixIntegration
|
||||
|
||||
# Initialize the integration
|
||||
wix = WixIntegration(
|
||||
refresh_token="YOUR_REFRESH_TOKEN",
|
||||
site_id="YOUR_SITE_ID"
|
||||
)
|
||||
|
||||
# Publish a blog post
|
||||
result = wix.publish_blog_post(
|
||||
title="My Blog Post",
|
||||
content="# Hello World\n\nThis is my blog post.",
|
||||
is_markdown=True,
|
||||
tags=["example", "blog"],
|
||||
categories=["Technology"],
|
||||
publish=True
|
||||
)
|
||||
|
||||
# Get the published post URL
|
||||
post_url = result.get("post", {}).get("url")
|
||||
print(f"Published at: {post_url}")
|
||||
```
|
||||
|
||||
### Using the Command-Line Interface
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export WIX_REFRESH_TOKEN="YOUR_REFRESH_TOKEN"
|
||||
export WIX_SITE_ID="YOUR_SITE_ID"
|
||||
|
||||
# List blog posts
|
||||
python -m lib.integrations.wix_cli list-posts
|
||||
|
||||
# Publish a blog post
|
||||
python -m lib.integrations.wix_cli publish-post \
|
||||
--title "My Blog Post" \
|
||||
--content-file blog.md \
|
||||
--is-markdown \
|
||||
--tags "example,blog" \
|
||||
--categories "Technology"
|
||||
|
||||
# Generate an SEO report
|
||||
python -m lib.integrations.wix_cli seo-report \
|
||||
--title "My Blog Post" \
|
||||
--keywords "example,blog,technology"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### WixIntegration
|
||||
|
||||
The main integration class that provides high-level methods for working with Wix blogs.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `publish_blog_post(title, content, ...)`: Publish a blog post
|
||||
- `upload_media(file_path, ...)`: Upload a media file
|
||||
- `get_seo_report(post_id, target_keywords)`: Generate an SEO report
|
||||
- `list_blog_posts(limit, offset, ...)`: List blog posts
|
||||
- `list_categories()`: List blog categories
|
||||
- `create_category(name, description)`: Create a blog category
|
||||
- `get_post_by_id(post_id)`: Get a blog post by ID
|
||||
- `get_post_by_title(title)`: Get a blog post by title
|
||||
- `delete_post(post_id)`: Delete a blog post
|
||||
|
||||
### WixAPIClient
|
||||
|
||||
Low-level client for interacting with the Wix API.
|
||||
|
||||
### WixBlogManager
|
||||
|
||||
Handles blog content management, including markdown processing and image handling.
|
||||
|
||||
### WixSEOOptimizer
|
||||
|
||||
Provides SEO analysis and optimization for blog posts.
|
||||
|
||||
## Error Handling
|
||||
|
||||
The integration includes comprehensive error handling:
|
||||
|
||||
- API errors are logged with detailed information
|
||||
- Authentication errors provide clear guidance
|
||||
- File handling errors include path information
|
||||
- Network errors include retry logic
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Store credentials securely**:
|
||||
- Use environment variables or a secure credential store
|
||||
- Don't hardcode credentials in your code
|
||||
|
||||
2. **Optimize images before upload**:
|
||||
- Compress images to reduce file size
|
||||
- Use appropriate image formats (JPEG for photos, PNG for graphics)
|
||||
|
||||
3. **SEO optimization**:
|
||||
- Use the SEO report to improve your content
|
||||
- Include relevant keywords in titles and headings
|
||||
- Add alt text to all images
|
||||
|
||||
4. **Content management**:
|
||||
- Use categories and tags consistently
|
||||
- Include featured images for better visual appeal
|
||||
- Write clear, concise meta descriptions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Authentication Errors**:
|
||||
- Ensure your refresh token is valid
|
||||
- Check that your site ID is correct
|
||||
- Verify that your app has the necessary permissions
|
||||
|
||||
2. **API Rate Limits**:
|
||||
- The Wix API has rate limits that may affect bulk operations
|
||||
- Add delays between requests if you're publishing many posts
|
||||
|
||||
3. **Image Upload Issues**:
|
||||
- Check that the image file exists and is readable
|
||||
- Verify that the image format is supported (JPEG, PNG, GIF)
|
||||
- Ensure the image file size is within Wix limits
|
||||
|
||||
4. **Content Formatting Issues**:
|
||||
- If using markdown, ensure it's valid
|
||||
- Check for special characters that might cause issues
|
||||
- Verify that HTML content is properly formatted
|
||||
|
||||
### Getting Help
|
||||
|
||||
If you encounter issues not covered here:
|
||||
|
||||
1. Check the logs for detailed error messages
|
||||
2. Consult the [Wix API Documentation](https://dev.wix.com/api/rest/getting-started)
|
||||
3. Contact Alwrity support for assistance
|
||||
|
||||
## License
|
||||
|
||||
This integration is part of the Alwrity platform and is subject to the same license terms.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [Wix REST API](https://dev.wix.com/api/rest) for providing the API endpoints
|
||||
- [Requests](https://docs.python-requests.org/) for HTTP functionality
|
||||
- [Markdown](https://python-markdown.github.io/) for markdown processing
|
||||
- [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/) for HTML parsing
|
||||
- [Streamlit](https://streamlit.io/) for the user interface
|
||||
@@ -1,841 +0,0 @@
|
||||
"""
|
||||
Wix API Client for Blog Management
|
||||
|
||||
This module provides a comprehensive client for interacting with the Wix API
|
||||
to manage blog posts, SEO settings, and media uploads.
|
||||
|
||||
Documentation: https://dev.wix.com/api/rest/getting-started
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import requests
|
||||
from typing import Dict, List, Optional, Union, Any, Tuple
|
||||
from datetime import datetime
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('wix_api_client')
|
||||
|
||||
class WixAPIClient:
|
||||
"""
|
||||
Client for interacting with the Wix API for blog management.
|
||||
|
||||
This client handles authentication, blog post creation/updating,
|
||||
media uploads, and SEO settings.
|
||||
"""
|
||||
|
||||
# Base URLs for different Wix API endpoints
|
||||
BASE_URL = "https://www.wixapis.com"
|
||||
OAUTH_URL = "https://www.wix.com/oauth"
|
||||
|
||||
# API Endpoints
|
||||
BLOG_API = "/blog/v3"
|
||||
MEDIA_API = "/site-media/v1"
|
||||
SEO_API = "/site-properties/v4/seo"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
refresh_token: Optional[str] = None,
|
||||
site_id: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Initialize the Wix API Client.
|
||||
|
||||
Args:
|
||||
api_key: Wix API key (optional if using refresh token)
|
||||
refresh_token: Wix refresh token for OAuth authentication
|
||||
site_id: Wix site ID
|
||||
"""
|
||||
self.api_key = api_key or os.environ.get('WIX_API_KEY')
|
||||
self.refresh_token = refresh_token or os.environ.get('WIX_REFRESH_TOKEN')
|
||||
self.site_id = site_id or os.environ.get('WIX_SITE_ID')
|
||||
self.access_token = None
|
||||
self.token_expiry = 0
|
||||
|
||||
if not self.refresh_token:
|
||||
logger.warning("No refresh token provided. Authentication will fail.")
|
||||
|
||||
if not self.site_id:
|
||||
logger.warning("No site ID provided. API calls will fail.")
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""
|
||||
Get the headers required for API requests.
|
||||
|
||||
Returns:
|
||||
Dict containing the necessary headers for Wix API requests
|
||||
"""
|
||||
# Ensure we have a valid access token
|
||||
self._ensure_valid_token()
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.access_token}",
|
||||
"wix-site-id": self.site_id,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
return headers
|
||||
|
||||
def _ensure_valid_token(self) -> None:
|
||||
"""
|
||||
Ensure we have a valid access token, refreshing if necessary.
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
# If token is expired or doesn't exist, refresh it
|
||||
if not self.access_token or current_time >= self.token_expiry:
|
||||
self._refresh_access_token()
|
||||
|
||||
def _refresh_access_token(self) -> None:
|
||||
"""
|
||||
Refresh the access token using the refresh token.
|
||||
"""
|
||||
if not self.refresh_token:
|
||||
raise ValueError("Refresh token is required for authentication")
|
||||
|
||||
url = f"{self.OAUTH_URL}/access"
|
||||
payload = {
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": self.refresh_token,
|
||||
"client_id": self.api_key if self.api_key else ""
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
self.access_token = data.get("access_token")
|
||||
|
||||
# Set token expiry (subtract 5 minutes for safety margin)
|
||||
expires_in = data.get("expires_in", 3600) # Default to 1 hour if not specified
|
||||
self.token_expiry = time.time() + expires_in - 300
|
||||
|
||||
logger.info("Successfully refreshed access token")
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Failed to refresh access token: {str(e)}")
|
||||
if response.text:
|
||||
logger.error(f"Response: {response.text}")
|
||||
raise
|
||||
|
||||
def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: Optional[Dict] = None,
|
||||
params: Optional[Dict] = None,
|
||||
files: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Make a request to the Wix API.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, PUT, DELETE)
|
||||
endpoint: API endpoint
|
||||
data: Request payload
|
||||
params: Query parameters
|
||||
files: Files to upload
|
||||
|
||||
Returns:
|
||||
Response data as dictionary
|
||||
"""
|
||||
url = f"{self.BASE_URL}{endpoint}"
|
||||
headers = self._get_headers()
|
||||
|
||||
# If we're uploading files, remove the Content-Type header
|
||||
if files:
|
||||
headers.pop("Content-Type", None)
|
||||
|
||||
try:
|
||||
response = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=headers,
|
||||
json=data,
|
||||
params=params,
|
||||
files=files
|
||||
)
|
||||
|
||||
# Log request details for debugging
|
||||
logger.debug(f"Request: {method} {url}")
|
||||
logger.debug(f"Headers: {headers}")
|
||||
if data:
|
||||
logger.debug(f"Data: {json.dumps(data)}")
|
||||
if params:
|
||||
logger.debug(f"Params: {params}")
|
||||
|
||||
# Handle response
|
||||
response.raise_for_status()
|
||||
|
||||
if response.content:
|
||||
return response.json()
|
||||
return {}
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.error(f"HTTP error: {str(e)}")
|
||||
if response.text:
|
||||
logger.error(f"Response: {response.text}")
|
||||
raise
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Request error: {str(e)}")
|
||||
raise
|
||||
|
||||
def list_posts(
|
||||
self,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
sort_field: str = "lastPublishedDate",
|
||||
sort_order: str = "desc",
|
||||
filter_by: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
List blog posts with pagination and sorting.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of posts to return (default: 50)
|
||||
offset: Pagination offset (default: 0)
|
||||
sort_field: Field to sort by (default: lastPublishedDate)
|
||||
sort_order: Sort order, 'asc' or 'desc' (default: desc)
|
||||
filter_by: Optional filter criteria
|
||||
|
||||
Returns:
|
||||
Dictionary containing blog posts and pagination info
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/posts/query"
|
||||
|
||||
payload = {
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"sort": [
|
||||
{
|
||||
"fieldName": sort_field,
|
||||
"order": sort_order
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if filter_by:
|
||||
payload["filter"] = filter_by
|
||||
|
||||
return self._make_request("POST", endpoint, data=payload)
|
||||
|
||||
def get_post(self, post_id: str) -> Dict:
|
||||
"""
|
||||
Get a specific blog post by ID.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
|
||||
Returns:
|
||||
Blog post data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/posts/{post_id}"
|
||||
return self._make_request("GET", endpoint)
|
||||
|
||||
def create_post(
|
||||
self,
|
||||
title: str,
|
||||
content: str,
|
||||
excerpt: Optional[str] = None,
|
||||
featured_image_id: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_data: Optional[Dict] = None,
|
||||
publish: bool = False
|
||||
) -> Dict:
|
||||
"""
|
||||
Create a new blog post.
|
||||
|
||||
Args:
|
||||
title: Post title
|
||||
content: Post content (HTML)
|
||||
excerpt: Post excerpt/summary
|
||||
featured_image_id: ID of the featured image (from media manager)
|
||||
tags: List of tags
|
||||
categories: List of category IDs
|
||||
seo_data: SEO settings for the post
|
||||
publish: Whether to publish the post immediately
|
||||
|
||||
Returns:
|
||||
Created blog post data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/posts"
|
||||
|
||||
# Prepare the post data
|
||||
post_data = {
|
||||
"post": {
|
||||
"title": title,
|
||||
"content": content,
|
||||
"excerpt": excerpt or "",
|
||||
"featured_image_id": featured_image_id,
|
||||
"tags": tags or [],
|
||||
"categoryIds": categories or []
|
||||
}
|
||||
}
|
||||
|
||||
# Add SEO data if provided
|
||||
if seo_data:
|
||||
post_data["post"]["seoData"] = seo_data
|
||||
|
||||
# Create the post
|
||||
response = self._make_request("POST", endpoint, data=post_data)
|
||||
|
||||
# Publish the post if requested
|
||||
if publish and response.get("post", {}).get("id"):
|
||||
post_id = response["post"]["id"]
|
||||
self.publish_post(post_id)
|
||||
# Refresh the post data to get the published version
|
||||
response = self.get_post(post_id)
|
||||
|
||||
return response
|
||||
|
||||
def update_post(
|
||||
self,
|
||||
post_id: str,
|
||||
title: Optional[str] = None,
|
||||
content: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
featured_image_id: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_data: Optional[Dict] = None,
|
||||
publish: bool = False
|
||||
) -> Dict:
|
||||
"""
|
||||
Update an existing blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the post to update
|
||||
title: New post title (optional)
|
||||
content: New post content (HTML) (optional)
|
||||
excerpt: New post excerpt/summary (optional)
|
||||
featured_image_id: New featured image ID (optional)
|
||||
tags: New list of tags (optional)
|
||||
categories: New list of category IDs (optional)
|
||||
seo_data: New SEO settings (optional)
|
||||
publish: Whether to publish the post after updating
|
||||
|
||||
Returns:
|
||||
Updated blog post data
|
||||
"""
|
||||
# First, get the current post data
|
||||
current_post = self.get_post(post_id)
|
||||
|
||||
if "post" not in current_post:
|
||||
raise ValueError(f"Post with ID {post_id} not found")
|
||||
|
||||
current_post_data = current_post["post"]
|
||||
|
||||
# Update only the fields that were provided
|
||||
update_data = {
|
||||
"post": {
|
||||
"id": post_id,
|
||||
"title": title if title is not None else current_post_data.get("title", ""),
|
||||
"content": content if content is not None else current_post_data.get("content", ""),
|
||||
"excerpt": excerpt if excerpt is not None else current_post_data.get("excerpt", ""),
|
||||
"featured_image_id": featured_image_id if featured_image_id is not None else current_post_data.get("featuredImageId"),
|
||||
"tags": tags if tags is not None else current_post_data.get("tags", []),
|
||||
"categoryIds": categories if categories is not None else current_post_data.get("categoryIds", [])
|
||||
}
|
||||
}
|
||||
|
||||
# Add SEO data if provided
|
||||
if seo_data:
|
||||
update_data["post"]["seoData"] = seo_data
|
||||
elif "seoData" in current_post_data:
|
||||
update_data["post"]["seoData"] = current_post_data["seoData"]
|
||||
|
||||
# Update the post
|
||||
endpoint = f"{self.BLOG_API}/posts/{post_id}"
|
||||
response = self._make_request("PATCH", endpoint, data=update_data)
|
||||
|
||||
# Publish the post if requested
|
||||
if publish:
|
||||
self.publish_post(post_id)
|
||||
# Refresh the post data to get the published version
|
||||
response = self.get_post(post_id)
|
||||
|
||||
return response
|
||||
|
||||
def delete_post(self, post_id: str) -> Dict:
|
||||
"""
|
||||
Delete a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the post to delete
|
||||
|
||||
Returns:
|
||||
Response data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/posts/{post_id}"
|
||||
return self._make_request("DELETE", endpoint)
|
||||
|
||||
def publish_post(self, post_id: str) -> Dict:
|
||||
"""
|
||||
Publish a draft blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the post to publish
|
||||
|
||||
Returns:
|
||||
Published post data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/posts/{post_id}/publish"
|
||||
return self._make_request("POST", endpoint)
|
||||
|
||||
def unpublish_post(self, post_id: str) -> Dict:
|
||||
"""
|
||||
Unpublish a published blog post (revert to draft).
|
||||
|
||||
Args:
|
||||
post_id: ID of the post to unpublish
|
||||
|
||||
Returns:
|
||||
Unpublished post data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/posts/{post_id}/unpublish"
|
||||
return self._make_request("POST", endpoint)
|
||||
|
||||
def list_categories(self) -> Dict:
|
||||
"""
|
||||
List all blog categories.
|
||||
|
||||
Returns:
|
||||
Dictionary containing blog categories
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/categories"
|
||||
return self._make_request("GET", endpoint)
|
||||
|
||||
def create_category(self, label: str, description: Optional[str] = None) -> Dict:
|
||||
"""
|
||||
Create a new blog category.
|
||||
|
||||
Args:
|
||||
label: Category name
|
||||
description: Category description (optional)
|
||||
|
||||
Returns:
|
||||
Created category data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/categories"
|
||||
|
||||
payload = {
|
||||
"category": {
|
||||
"label": label,
|
||||
"description": description or ""
|
||||
}
|
||||
}
|
||||
|
||||
return self._make_request("POST", endpoint, data=payload)
|
||||
|
||||
def update_category(
|
||||
self,
|
||||
category_id: str,
|
||||
label: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Update an existing blog category.
|
||||
|
||||
Args:
|
||||
category_id: ID of the category to update
|
||||
label: New category name (optional)
|
||||
description: New category description (optional)
|
||||
|
||||
Returns:
|
||||
Updated category data
|
||||
"""
|
||||
# First, get the current category data
|
||||
current_categories = self.list_categories()
|
||||
|
||||
current_category = None
|
||||
for category in current_categories.get("categories", []):
|
||||
if category.get("id") == category_id:
|
||||
current_category = category
|
||||
break
|
||||
|
||||
if not current_category:
|
||||
raise ValueError(f"Category with ID {category_id} not found")
|
||||
|
||||
# Update only the fields that were provided
|
||||
update_data = {
|
||||
"category": {
|
||||
"id": category_id,
|
||||
"label": label if label is not None else current_category.get("label", ""),
|
||||
"description": description if description is not None else current_category.get("description", "")
|
||||
}
|
||||
}
|
||||
|
||||
endpoint = f"{self.BLOG_API}/categories/{category_id}"
|
||||
return self._make_request("PATCH", endpoint, data=update_data)
|
||||
|
||||
def delete_category(self, category_id: str) -> Dict:
|
||||
"""
|
||||
Delete a blog category.
|
||||
|
||||
Args:
|
||||
category_id: ID of the category to delete
|
||||
|
||||
Returns:
|
||||
Response data
|
||||
"""
|
||||
endpoint = f"{self.BLOG_API}/categories/{category_id}"
|
||||
return self._make_request("DELETE", endpoint)
|
||||
|
||||
def upload_image(
|
||||
self,
|
||||
file_path: str,
|
||||
title: Optional[str] = None,
|
||||
alt_text: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Upload an image to the Wix media manager.
|
||||
|
||||
Args:
|
||||
file_path: Path to the image file
|
||||
title: Image title (optional)
|
||||
alt_text: Image alt text for accessibility (optional)
|
||||
description: Image description (optional)
|
||||
|
||||
Returns:
|
||||
Uploaded image data
|
||||
"""
|
||||
# Check if file exists
|
||||
if not os.path.isfile(file_path):
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
# Get file name and mime type
|
||||
file_name = os.path.basename(file_path)
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
|
||||
if not mime_type or not mime_type.startswith('image/'):
|
||||
raise ValueError(f"File does not appear to be an image: {file_path}")
|
||||
|
||||
# Prepare metadata
|
||||
metadata = {
|
||||
"title": title or file_name,
|
||||
"altText": alt_text or "",
|
||||
"description": description or ""
|
||||
}
|
||||
|
||||
# First, get an upload URL
|
||||
endpoint = f"{self.MEDIA_API}/files/upload/url"
|
||||
upload_url_response = self._make_request("POST", endpoint, data={
|
||||
"mimeType": mime_type,
|
||||
"fileName": file_name
|
||||
})
|
||||
|
||||
if "uploadUrl" not in upload_url_response:
|
||||
raise ValueError("Failed to get upload URL")
|
||||
|
||||
upload_url = upload_url_response["uploadUrl"]
|
||||
|
||||
# Upload the file to the provided URL
|
||||
with open(file_path, 'rb') as file:
|
||||
upload_response = requests.post(
|
||||
upload_url,
|
||||
files={'file': (file_name, file, mime_type)},
|
||||
headers={"Content-Type": mime_type}
|
||||
)
|
||||
|
||||
upload_response.raise_for_status()
|
||||
|
||||
# Complete the upload with metadata
|
||||
endpoint = f"{self.MEDIA_API}/files"
|
||||
complete_data = {
|
||||
"uploadToken": upload_url_response.get("uploadToken"),
|
||||
"mediaOptions": {
|
||||
"mimeType": mime_type,
|
||||
"fileName": file_name,
|
||||
"mediaType": "IMAGE",
|
||||
"title": metadata["title"],
|
||||
"description": metadata["description"],
|
||||
"alt": metadata["altText"]
|
||||
}
|
||||
}
|
||||
|
||||
return self._make_request("POST", endpoint, data=complete_data)
|
||||
|
||||
def get_media_item(self, media_id: str) -> Dict:
|
||||
"""
|
||||
Get details of a specific media item.
|
||||
|
||||
Args:
|
||||
media_id: ID of the media item
|
||||
|
||||
Returns:
|
||||
Media item data
|
||||
"""
|
||||
endpoint = f"{self.MEDIA_API}/files/{media_id}"
|
||||
return self._make_request("GET", endpoint)
|
||||
|
||||
def list_media_items(
|
||||
self,
|
||||
media_type: str = "IMAGE",
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> Dict:
|
||||
"""
|
||||
List media items with pagination.
|
||||
|
||||
Args:
|
||||
media_type: Type of media to list (IMAGE, VIDEO, AUDIO, DOCUMENT)
|
||||
limit: Maximum number of items to return
|
||||
offset: Pagination offset
|
||||
|
||||
Returns:
|
||||
Dictionary containing media items and pagination info
|
||||
"""
|
||||
endpoint = f"{self.MEDIA_API}/files/query"
|
||||
|
||||
payload = {
|
||||
"query": {
|
||||
"paging": {
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
},
|
||||
"filter": {
|
||||
"mediaType": media_type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self._make_request("POST", endpoint, data=payload)
|
||||
|
||||
def delete_media_item(self, media_id: str) -> Dict:
|
||||
"""
|
||||
Delete a media item.
|
||||
|
||||
Args:
|
||||
media_id: ID of the media item to delete
|
||||
|
||||
Returns:
|
||||
Response data
|
||||
"""
|
||||
endpoint = f"{self.MEDIA_API}/files/{media_id}"
|
||||
return self._make_request("DELETE", endpoint)
|
||||
|
||||
def get_seo_settings(self, page_url: str) -> Dict:
|
||||
"""
|
||||
Get SEO settings for a specific page.
|
||||
|
||||
Args:
|
||||
page_url: URL path of the page (e.g., "/blog/my-post")
|
||||
|
||||
Returns:
|
||||
SEO settings data
|
||||
"""
|
||||
endpoint = f"{self.SEO_API}/sites/{self.site_id}/url/{page_url}"
|
||||
return self._make_request("GET", endpoint)
|
||||
|
||||
def update_seo_settings(
|
||||
self,
|
||||
page_url: str,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
keywords: Optional[List[str]] = None,
|
||||
og_image_url: Optional[str] = None,
|
||||
structured_data: Optional[Dict] = None,
|
||||
no_index: Optional[bool] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Update SEO settings for a specific page.
|
||||
|
||||
Args:
|
||||
page_url: URL path of the page (e.g., "/blog/my-post")
|
||||
title: SEO title
|
||||
description: SEO description
|
||||
keywords: SEO keywords
|
||||
og_image_url: Open Graph image URL
|
||||
structured_data: Structured data (JSON-LD)
|
||||
no_index: Whether to prevent indexing by search engines
|
||||
|
||||
Returns:
|
||||
Updated SEO settings data
|
||||
"""
|
||||
# First, get current SEO settings
|
||||
try:
|
||||
current_settings = self.get_seo_settings(page_url)
|
||||
except:
|
||||
# If the page doesn't exist yet, start with empty settings
|
||||
current_settings = {"tags": {}}
|
||||
|
||||
# Prepare the update data
|
||||
seo_data = {
|
||||
"tags": {}
|
||||
}
|
||||
|
||||
# Update only the fields that were provided
|
||||
if title is not None:
|
||||
seo_data["tags"]["title"] = title
|
||||
elif "title" in current_settings.get("tags", {}):
|
||||
seo_data["tags"]["title"] = current_settings["tags"]["title"]
|
||||
|
||||
if description is not None:
|
||||
seo_data["tags"]["description"] = description
|
||||
elif "description" in current_settings.get("tags", {}):
|
||||
seo_data["tags"]["description"] = current_settings["tags"]["description"]
|
||||
|
||||
if keywords is not None:
|
||||
seo_data["tags"]["keywords"] = ", ".join(keywords)
|
||||
elif "keywords" in current_settings.get("tags", {}):
|
||||
seo_data["tags"]["keywords"] = current_settings["tags"]["keywords"]
|
||||
|
||||
if og_image_url is not None:
|
||||
seo_data["tags"]["og:image"] = og_image_url
|
||||
elif "og:image" in current_settings.get("tags", {}):
|
||||
seo_data["tags"]["og:image"] = current_settings["tags"]["og:image"]
|
||||
|
||||
if structured_data is not None:
|
||||
seo_data["tags"]["jsonld"] = json.dumps(structured_data)
|
||||
elif "jsonld" in current_settings.get("tags", {}):
|
||||
seo_data["tags"]["jsonld"] = current_settings["tags"]["jsonld"]
|
||||
|
||||
if no_index is not None:
|
||||
seo_data["tags"]["robots"] = "noindex" if no_index else "index"
|
||||
elif "robots" in current_settings.get("tags", {}):
|
||||
seo_data["tags"]["robots"] = current_settings["tags"]["robots"]
|
||||
|
||||
endpoint = f"{self.SEO_API}/sites/{self.site_id}/url/{page_url}"
|
||||
return self._make_request("PUT", endpoint, data=seo_data)
|
||||
|
||||
def create_blog_post_with_image(
|
||||
self,
|
||||
title: str,
|
||||
content: str,
|
||||
image_path: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
publish: bool = False
|
||||
) -> Dict:
|
||||
"""
|
||||
Create a blog post with an optional featured image in one operation.
|
||||
|
||||
Args:
|
||||
title: Post title
|
||||
content: Post content (HTML)
|
||||
image_path: Path to featured image (optional)
|
||||
excerpt: Post excerpt/summary (optional)
|
||||
tags: List of tags (optional)
|
||||
categories: List of category IDs (optional)
|
||||
seo_title: SEO title (optional)
|
||||
seo_description: SEO description (optional)
|
||||
seo_keywords: SEO keywords (optional)
|
||||
publish: Whether to publish the post immediately (optional)
|
||||
|
||||
Returns:
|
||||
Created blog post data
|
||||
"""
|
||||
# Upload image if provided
|
||||
featured_image_id = None
|
||||
if image_path and os.path.isfile(image_path):
|
||||
try:
|
||||
image_response = self.upload_image(
|
||||
file_path=image_path,
|
||||
title=title,
|
||||
alt_text=title
|
||||
)
|
||||
featured_image_id = image_response.get("file", {}).get("id")
|
||||
logger.info(f"Uploaded image with ID: {featured_image_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload image: {str(e)}")
|
||||
|
||||
# Prepare SEO data
|
||||
seo_data = None
|
||||
if seo_title or seo_description or seo_keywords:
|
||||
seo_data = {
|
||||
"title": seo_title or title,
|
||||
"description": seo_description or excerpt or "",
|
||||
"keywords": seo_keywords or tags or []
|
||||
}
|
||||
|
||||
# Create the blog post
|
||||
return self.create_post(
|
||||
title=title,
|
||||
content=content,
|
||||
excerpt=excerpt,
|
||||
featured_image_id=featured_image_id,
|
||||
tags=tags,
|
||||
categories=categories,
|
||||
seo_data=seo_data,
|
||||
publish=publish
|
||||
)
|
||||
|
||||
def get_or_create_category(self, category_name: str) -> str:
|
||||
"""
|
||||
Get a category ID by name, creating it if it doesn't exist.
|
||||
|
||||
Args:
|
||||
category_name: Name of the category
|
||||
|
||||
Returns:
|
||||
Category ID
|
||||
"""
|
||||
# List all categories
|
||||
categories_response = self.list_categories()
|
||||
categories = categories_response.get("categories", [])
|
||||
|
||||
# Check if category exists
|
||||
for category in categories:
|
||||
if category.get("label", "").lower() == category_name.lower():
|
||||
return category.get("id")
|
||||
|
||||
# Create category if it doesn't exist
|
||||
create_response = self.create_category(label=category_name)
|
||||
return create_response.get("category", {}).get("id")
|
||||
|
||||
def get_post_by_slug(self, slug: str) -> Optional[Dict]:
|
||||
"""
|
||||
Find a post by its slug.
|
||||
|
||||
Args:
|
||||
slug: Post slug
|
||||
|
||||
Returns:
|
||||
Post data or None if not found
|
||||
"""
|
||||
# List posts with a filter for the slug
|
||||
filter_by = {
|
||||
"slug": {
|
||||
"$eq": slug
|
||||
}
|
||||
}
|
||||
|
||||
response = self.list_posts(limit=1, filter_by=filter_by)
|
||||
posts = response.get("posts", [])
|
||||
|
||||
if posts:
|
||||
return posts[0]
|
||||
return None
|
||||
|
||||
def get_post_url(self, post_id: str) -> str:
|
||||
"""
|
||||
Get the full URL for a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
|
||||
Returns:
|
||||
Full URL to the blog post
|
||||
"""
|
||||
post_data = self.get_post(post_id)
|
||||
slug = post_data.get("post", {}).get("slug", "")
|
||||
|
||||
# Get the blog URL prefix
|
||||
# This is a simplification - in reality, you might need to get this from site settings
|
||||
return f"/blog/{slug}"
|
||||
@@ -1,720 +0,0 @@
|
||||
"""
|
||||
Wix Blog Manager
|
||||
|
||||
This module provides high-level functions for managing blog content on Wix,
|
||||
including content creation, SEO optimization, and media management.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import tempfile
|
||||
import requests
|
||||
from typing import Dict, List, Optional, Union, Any, Tuple
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import markdown
|
||||
import html2text
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from .wix_api_client import WixAPIClient
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('wix_blog_manager')
|
||||
|
||||
class WixBlogManager:
|
||||
"""
|
||||
High-level manager for Wix blog content.
|
||||
|
||||
This class provides convenient methods for common blog management tasks,
|
||||
building on the lower-level WixAPIClient.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
refresh_token: Optional[str] = None,
|
||||
site_id: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Initialize the Wix Blog Manager.
|
||||
|
||||
Args:
|
||||
api_key: Wix API key (optional if using refresh token)
|
||||
refresh_token: Wix refresh token for OAuth authentication
|
||||
site_id: Wix site ID
|
||||
"""
|
||||
self.client = WixAPIClient(api_key, refresh_token, site_id)
|
||||
|
||||
def publish_markdown_post(
|
||||
self,
|
||||
title: str,
|
||||
markdown_content: str,
|
||||
featured_image_path: Optional[str] = None,
|
||||
featured_image_url: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
publish: bool = False
|
||||
) -> Dict:
|
||||
"""
|
||||
Publish a blog post from markdown content.
|
||||
|
||||
Args:
|
||||
title: Post title
|
||||
markdown_content: Post content in markdown format
|
||||
featured_image_path: Local path to featured image (optional)
|
||||
featured_image_url: URL of featured image to download (optional)
|
||||
excerpt: Post excerpt/summary (optional)
|
||||
tags: List of tags (optional)
|
||||
categories: List of category names (optional)
|
||||
seo_title: SEO title (optional)
|
||||
seo_description: SEO description (optional)
|
||||
seo_keywords: SEO keywords (optional)
|
||||
publish: Whether to publish the post immediately (optional)
|
||||
|
||||
Returns:
|
||||
Published blog post data
|
||||
"""
|
||||
# Convert markdown to HTML
|
||||
html_content = self._markdown_to_html(markdown_content)
|
||||
|
||||
# Process images in the content
|
||||
html_content, embedded_images = self._process_content_images(html_content)
|
||||
|
||||
# Handle featured image
|
||||
featured_image_id = None
|
||||
temp_image_path = None
|
||||
|
||||
if featured_image_url and not featured_image_path:
|
||||
# Download the image from URL
|
||||
try:
|
||||
temp_image_path = self._download_image(featured_image_url)
|
||||
featured_image_path = temp_image_path
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download featured image: {str(e)}")
|
||||
|
||||
if featured_image_path:
|
||||
try:
|
||||
image_response = self.client.upload_image(
|
||||
file_path=featured_image_path,
|
||||
title=title,
|
||||
alt_text=title
|
||||
)
|
||||
featured_image_id = image_response.get("file", {}).get("id")
|
||||
logger.info(f"Uploaded featured image with ID: {featured_image_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload featured image: {str(e)}")
|
||||
|
||||
# Clean up temporary file if created
|
||||
if temp_image_path and os.path.exists(temp_image_path):
|
||||
try:
|
||||
os.remove(temp_image_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Process categories - convert names to IDs
|
||||
category_ids = []
|
||||
if categories:
|
||||
for category_name in categories:
|
||||
try:
|
||||
category_id = self.client.get_or_create_category(category_name)
|
||||
if category_id:
|
||||
category_ids.append(category_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process category '{category_name}': {str(e)}")
|
||||
|
||||
# Generate excerpt if not provided
|
||||
if not excerpt:
|
||||
excerpt = self._generate_excerpt(markdown_content)
|
||||
|
||||
# Prepare SEO data
|
||||
seo_data = None
|
||||
if seo_title or seo_description or seo_keywords:
|
||||
seo_data = {
|
||||
"title": seo_title or title,
|
||||
"description": seo_description or excerpt or "",
|
||||
"keywords": seo_keywords or tags or []
|
||||
}
|
||||
|
||||
# Create the blog post
|
||||
response = self.client.create_post(
|
||||
title=title,
|
||||
content=html_content,
|
||||
excerpt=excerpt,
|
||||
featured_image_id=featured_image_id,
|
||||
tags=tags,
|
||||
categories=category_ids,
|
||||
seo_data=seo_data,
|
||||
publish=publish
|
||||
)
|
||||
|
||||
# Update SEO settings if the post was published
|
||||
if publish and response.get("post", {}).get("id"):
|
||||
post_id = response["post"]["id"]
|
||||
post_url = self.client.get_post_url(post_id)
|
||||
|
||||
try:
|
||||
self.client.update_seo_settings(
|
||||
page_url=post_url,
|
||||
title=seo_title or title,
|
||||
description=seo_description or excerpt or "",
|
||||
keywords=seo_keywords or tags,
|
||||
og_image_url=featured_image_url
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update SEO settings: {str(e)}")
|
||||
|
||||
return response
|
||||
|
||||
def update_markdown_post(
|
||||
self,
|
||||
post_id: str,
|
||||
title: Optional[str] = None,
|
||||
markdown_content: Optional[str] = None,
|
||||
featured_image_path: Optional[str] = None,
|
||||
featured_image_url: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
publish: bool = False
|
||||
) -> Dict:
|
||||
"""
|
||||
Update an existing blog post with markdown content.
|
||||
|
||||
Args:
|
||||
post_id: ID of the post to update
|
||||
title: New post title (optional)
|
||||
markdown_content: New post content in markdown format (optional)
|
||||
featured_image_path: Local path to new featured image (optional)
|
||||
featured_image_url: URL of new featured image to download (optional)
|
||||
excerpt: New post excerpt/summary (optional)
|
||||
tags: New list of tags (optional)
|
||||
categories: New list of category names (optional)
|
||||
seo_title: New SEO title (optional)
|
||||
seo_description: New SEO description (optional)
|
||||
seo_keywords: New SEO keywords (optional)
|
||||
publish: Whether to publish the post after updating (optional)
|
||||
|
||||
Returns:
|
||||
Updated blog post data
|
||||
"""
|
||||
# Get current post data
|
||||
current_post = self.client.get_post(post_id)
|
||||
if "post" not in current_post:
|
||||
raise ValueError(f"Post with ID {post_id} not found")
|
||||
|
||||
# Convert markdown to HTML if provided
|
||||
html_content = None
|
||||
if markdown_content:
|
||||
html_content = self._markdown_to_html(markdown_content)
|
||||
# Process images in the content
|
||||
html_content, embedded_images = self._process_content_images(html_content)
|
||||
|
||||
# Handle featured image
|
||||
featured_image_id = None
|
||||
temp_image_path = None
|
||||
|
||||
if featured_image_url and not featured_image_path:
|
||||
# Download the image from URL
|
||||
try:
|
||||
temp_image_path = self._download_image(featured_image_url)
|
||||
featured_image_path = temp_image_path
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download featured image: {str(e)}")
|
||||
|
||||
if featured_image_path:
|
||||
try:
|
||||
image_response = self.client.upload_image(
|
||||
file_path=featured_image_path,
|
||||
title=title or current_post["post"].get("title", ""),
|
||||
alt_text=title or current_post["post"].get("title", "")
|
||||
)
|
||||
featured_image_id = image_response.get("file", {}).get("id")
|
||||
logger.info(f"Uploaded featured image with ID: {featured_image_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to upload featured image: {str(e)}")
|
||||
|
||||
# Clean up temporary file if created
|
||||
if temp_image_path and os.path.exists(temp_image_path):
|
||||
try:
|
||||
os.remove(temp_image_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Process categories - convert names to IDs
|
||||
category_ids = None
|
||||
if categories:
|
||||
category_ids = []
|
||||
for category_name in categories:
|
||||
try:
|
||||
category_id = self.client.get_or_create_category(category_name)
|
||||
if category_id:
|
||||
category_ids.append(category_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process category '{category_name}': {str(e)}")
|
||||
|
||||
# Generate excerpt if not provided but markdown is
|
||||
if not excerpt and markdown_content:
|
||||
excerpt = self._generate_excerpt(markdown_content)
|
||||
|
||||
# Prepare SEO data
|
||||
seo_data = None
|
||||
if seo_title or seo_description or seo_keywords:
|
||||
seo_data = {
|
||||
"title": seo_title or title or current_post["post"].get("title", ""),
|
||||
"description": seo_description or excerpt or current_post["post"].get("excerpt", ""),
|
||||
"keywords": seo_keywords or tags or current_post["post"].get("tags", [])
|
||||
}
|
||||
|
||||
# Update the blog post
|
||||
response = self.client.update_post(
|
||||
post_id=post_id,
|
||||
title=title,
|
||||
content=html_content,
|
||||
excerpt=excerpt,
|
||||
featured_image_id=featured_image_id,
|
||||
tags=tags,
|
||||
categories=category_ids,
|
||||
seo_data=seo_data,
|
||||
publish=publish
|
||||
)
|
||||
|
||||
# Update SEO settings if needed
|
||||
if (seo_title or seo_description or seo_keywords or featured_image_url):
|
||||
post_url = self.client.get_post_url(post_id)
|
||||
|
||||
try:
|
||||
self.client.update_seo_settings(
|
||||
page_url=post_url,
|
||||
title=seo_title or title,
|
||||
description=seo_description or excerpt,
|
||||
keywords=seo_keywords or tags,
|
||||
og_image_url=featured_image_url
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update SEO settings: {str(e)}")
|
||||
|
||||
return response
|
||||
|
||||
def find_post_by_title(self, title: str) -> Optional[Dict]:
|
||||
"""
|
||||
Find a post by its title (exact match).
|
||||
|
||||
Args:
|
||||
title: Post title to search for
|
||||
|
||||
Returns:
|
||||
Post data or None if not found
|
||||
"""
|
||||
# List all posts (this is inefficient but Wix API doesn't support filtering by title)
|
||||
# In a production environment, you might want to implement pagination
|
||||
response = self.client.list_posts(limit=100)
|
||||
posts = response.get("posts", [])
|
||||
|
||||
for post in posts:
|
||||
if post.get("title") == title:
|
||||
return post
|
||||
|
||||
return None
|
||||
|
||||
def publish_or_update_markdown_post(
|
||||
self,
|
||||
title: str,
|
||||
markdown_content: str,
|
||||
featured_image_path: Optional[str] = None,
|
||||
featured_image_url: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
publish: bool = False,
|
||||
update_if_exists: bool = True
|
||||
) -> Dict:
|
||||
"""
|
||||
Publish a new post or update an existing one with the same title.
|
||||
|
||||
Args:
|
||||
title: Post title
|
||||
markdown_content: Post content in markdown format
|
||||
featured_image_path: Local path to featured image (optional)
|
||||
featured_image_url: URL of featured image to download (optional)
|
||||
excerpt: Post excerpt/summary (optional)
|
||||
tags: List of tags (optional)
|
||||
categories: List of category names (optional)
|
||||
seo_title: SEO title (optional)
|
||||
seo_description: SEO description (optional)
|
||||
seo_keywords: SEO keywords (optional)
|
||||
publish: Whether to publish the post immediately (optional)
|
||||
update_if_exists: Whether to update an existing post with the same title (optional)
|
||||
|
||||
Returns:
|
||||
Published or updated blog post data
|
||||
"""
|
||||
# Check if a post with this title already exists
|
||||
existing_post = self.find_post_by_title(title)
|
||||
|
||||
if existing_post and update_if_exists:
|
||||
# Update existing post
|
||||
logger.info(f"Updating existing post with title: {title}")
|
||||
return self.update_markdown_post(
|
||||
post_id=existing_post["id"],
|
||||
title=title,
|
||||
markdown_content=markdown_content,
|
||||
featured_image_path=featured_image_path,
|
||||
featured_image_url=featured_image_url,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=categories,
|
||||
seo_title=seo_title,
|
||||
seo_description=seo_description,
|
||||
seo_keywords=seo_keywords,
|
||||
publish=publish
|
||||
)
|
||||
else:
|
||||
# Create new post
|
||||
logger.info(f"Creating new post with title: {title}")
|
||||
return self.publish_markdown_post(
|
||||
title=title,
|
||||
markdown_content=markdown_content,
|
||||
featured_image_path=featured_image_path,
|
||||
featured_image_url=featured_image_url,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=categories,
|
||||
seo_title=seo_title,
|
||||
seo_description=seo_description,
|
||||
seo_keywords=seo_keywords,
|
||||
publish=publish
|
||||
)
|
||||
|
||||
def optimize_seo_for_post(
|
||||
self,
|
||||
post_id: str,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
og_image_url: Optional[str] = None,
|
||||
structured_data: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Optimize SEO settings for an existing blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
seo_title: SEO title (optional)
|
||||
seo_description: SEO description (optional)
|
||||
seo_keywords: SEO keywords (optional)
|
||||
og_image_url: Open Graph image URL (optional)
|
||||
structured_data: Structured data (JSON-LD) (optional)
|
||||
|
||||
Returns:
|
||||
Updated SEO settings data
|
||||
"""
|
||||
# Get the post URL
|
||||
post_url = self.client.get_post_url(post_id)
|
||||
|
||||
# Update SEO settings
|
||||
return self.client.update_seo_settings(
|
||||
page_url=post_url,
|
||||
title=seo_title,
|
||||
description=seo_description,
|
||||
keywords=seo_keywords,
|
||||
og_image_url=og_image_url,
|
||||
structured_data=structured_data
|
||||
)
|
||||
|
||||
def generate_structured_data(
|
||||
self,
|
||||
post_id: str,
|
||||
author_name: str,
|
||||
publisher_name: str,
|
||||
publisher_logo_url: str
|
||||
) -> Dict:
|
||||
"""
|
||||
Generate structured data (JSON-LD) for a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
author_name: Name of the author
|
||||
publisher_name: Name of the publisher
|
||||
publisher_logo_url: URL of the publisher's logo
|
||||
|
||||
Returns:
|
||||
Structured data as a dictionary
|
||||
"""
|
||||
# Get post data
|
||||
post_data = self.client.get_post(post_id)
|
||||
post = post_data.get("post", {})
|
||||
|
||||
# Get post URL
|
||||
post_url = self.client.get_post_url(post_id)
|
||||
|
||||
# Create structured data
|
||||
structured_data = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": post.get("title", ""),
|
||||
"description": post.get("excerpt", ""),
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": author_name
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": publisher_name,
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": publisher_logo_url
|
||||
}
|
||||
},
|
||||
"datePublished": post.get("publishedDate", ""),
|
||||
"dateModified": post.get("lastPublishedDate", "")
|
||||
}
|
||||
|
||||
# Add featured image if available
|
||||
if post.get("featuredImageId"):
|
||||
try:
|
||||
media_item = self.client.get_media_item(post["featuredImageId"])
|
||||
image_url = media_item.get("file", {}).get("url", "")
|
||||
if image_url:
|
||||
structured_data["image"] = image_url
|
||||
except:
|
||||
pass
|
||||
|
||||
return structured_data
|
||||
|
||||
def apply_structured_data_to_post(
|
||||
self,
|
||||
post_id: str,
|
||||
author_name: str,
|
||||
publisher_name: str,
|
||||
publisher_logo_url: str
|
||||
) -> Dict:
|
||||
"""
|
||||
Generate and apply structured data to a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
author_name: Name of the author
|
||||
publisher_name: Name of the publisher
|
||||
publisher_logo_url: URL of the publisher's logo
|
||||
|
||||
Returns:
|
||||
Updated SEO settings data
|
||||
"""
|
||||
# Generate structured data
|
||||
structured_data = self.generate_structured_data(
|
||||
post_id=post_id,
|
||||
author_name=author_name,
|
||||
publisher_name=publisher_name,
|
||||
publisher_logo_url=publisher_logo_url
|
||||
)
|
||||
|
||||
# Get the post URL
|
||||
post_url = self.client.get_post_url(post_id)
|
||||
|
||||
# Update SEO settings with structured data
|
||||
return self.client.update_seo_settings(
|
||||
page_url=post_url,
|
||||
structured_data=structured_data
|
||||
)
|
||||
|
||||
# Helper methods
|
||||
|
||||
def _markdown_to_html(self, markdown_content: str) -> str:
|
||||
"""
|
||||
Convert markdown content to HTML.
|
||||
|
||||
Args:
|
||||
markdown_content: Content in markdown format
|
||||
|
||||
Returns:
|
||||
HTML content
|
||||
"""
|
||||
# Use the markdown library to convert to HTML
|
||||
html = markdown.markdown(
|
||||
markdown_content,
|
||||
extensions=['extra', 'codehilite', 'tables', 'toc']
|
||||
)
|
||||
|
||||
return html
|
||||
|
||||
def _html_to_markdown(self, html_content: str) -> str:
|
||||
"""
|
||||
Convert HTML content to markdown.
|
||||
|
||||
Args:
|
||||
html_content: Content in HTML format
|
||||
|
||||
Returns:
|
||||
Markdown content
|
||||
"""
|
||||
# Use html2text to convert HTML to markdown
|
||||
h = html2text.HTML2Text()
|
||||
h.ignore_links = False
|
||||
h.ignore_images = False
|
||||
h.ignore_tables = False
|
||||
h.ignore_emphasis = False
|
||||
|
||||
return h.handle(html_content)
|
||||
|
||||
def _process_content_images(self, html_content: str) -> Tuple[str, List[Dict]]:
|
||||
"""
|
||||
Process images in HTML content, uploading them to Wix and replacing URLs.
|
||||
|
||||
Args:
|
||||
html_content: HTML content with image tags
|
||||
|
||||
Returns:
|
||||
Tuple of (updated HTML content, list of uploaded image data)
|
||||
"""
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
img_tags = soup.find_all('img')
|
||||
uploaded_images = []
|
||||
|
||||
for img in img_tags:
|
||||
src = img.get('src', '')
|
||||
alt = img.get('alt', '')
|
||||
|
||||
# Skip images that are already hosted on Wix
|
||||
if 'wixstatic.com' in src:
|
||||
continue
|
||||
|
||||
# Handle images with data URLs
|
||||
if src.startswith('data:image'):
|
||||
logger.info("Skipping data URL image - not supported in this implementation")
|
||||
continue
|
||||
|
||||
# Handle remote images
|
||||
if src.startswith('http://') or src.startswith('https://'):
|
||||
try:
|
||||
# Download the image
|
||||
temp_path = self._download_image(src)
|
||||
|
||||
# Upload to Wix
|
||||
image_response = self.client.upload_image(
|
||||
file_path=temp_path,
|
||||
title=alt or "Blog image",
|
||||
alt_text=alt or "Blog image"
|
||||
)
|
||||
|
||||
# Get the new URL
|
||||
new_url = image_response.get("file", {}).get("url", "")
|
||||
|
||||
if new_url:
|
||||
# Replace the src attribute
|
||||
img['src'] = new_url
|
||||
uploaded_images.append({
|
||||
'original_url': src,
|
||||
'wix_url': new_url,
|
||||
'wix_id': image_response.get("file", {}).get("id", "")
|
||||
})
|
||||
|
||||
# Clean up temp file
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process image {src}: {str(e)}")
|
||||
|
||||
# Handle local images (not implemented in this version)
|
||||
else:
|
||||
logger.info(f"Skipping local image {src} - not supported in this implementation")
|
||||
|
||||
# Return the updated HTML
|
||||
return str(soup), uploaded_images
|
||||
|
||||
def _download_image(self, url: str) -> str:
|
||||
"""
|
||||
Download an image from a URL to a temporary file.
|
||||
|
||||
Args:
|
||||
url: URL of the image
|
||||
|
||||
Returns:
|
||||
Path to the downloaded temporary file
|
||||
"""
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
# Determine file extension
|
||||
content_type = response.headers.get('content-type', '')
|
||||
extension = '.jpg' # Default
|
||||
|
||||
if 'image/jpeg' in content_type:
|
||||
extension = '.jpg'
|
||||
elif 'image/png' in content_type:
|
||||
extension = '.png'
|
||||
elif 'image/gif' in content_type:
|
||||
extension = '.gif'
|
||||
elif 'image/webp' in content_type:
|
||||
extension = '.webp'
|
||||
|
||||
# Create a temporary file
|
||||
fd, temp_path = tempfile.mkstemp(suffix=extension)
|
||||
os.close(fd)
|
||||
|
||||
# Write the image data to the file
|
||||
with open(temp_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
return temp_path
|
||||
|
||||
def _generate_excerpt(self, markdown_content: str, max_length: int = 160) -> str:
|
||||
"""
|
||||
Generate an excerpt from markdown content.
|
||||
|
||||
Args:
|
||||
markdown_content: Content in markdown format
|
||||
max_length: Maximum length of the excerpt
|
||||
|
||||
Returns:
|
||||
Generated excerpt
|
||||
"""
|
||||
# Convert markdown to plain text
|
||||
h = html2text.HTML2Text()
|
||||
h.ignore_links = True
|
||||
h.ignore_images = True
|
||||
h.ignore_tables = True
|
||||
h.ignore_emphasis = True
|
||||
|
||||
# First convert markdown to HTML, then HTML to plain text
|
||||
html = markdown.markdown(markdown_content)
|
||||
plain_text = h.handle(html)
|
||||
|
||||
# Clean up the text
|
||||
plain_text = re.sub(r'\s+', ' ', plain_text).strip()
|
||||
|
||||
# Truncate to max_length
|
||||
if len(plain_text) <= max_length:
|
||||
return plain_text
|
||||
|
||||
# Try to truncate at a sentence boundary
|
||||
sentences = re.split(r'(?<=[.!?])\s+', plain_text)
|
||||
excerpt = ""
|
||||
|
||||
for sentence in sentences:
|
||||
if len(excerpt + sentence) <= max_length:
|
||||
excerpt += sentence + " "
|
||||
else:
|
||||
break
|
||||
|
||||
# If we couldn't get a full sentence, just truncate
|
||||
if not excerpt:
|
||||
excerpt = plain_text[:max_length-3] + "..."
|
||||
|
||||
return excerpt.strip()
|
||||
@@ -1,350 +0,0 @@
|
||||
"""
|
||||
Wix Blog Publisher for Alwrity
|
||||
|
||||
This module integrates the Wix API with the Alwrity AI Writer platform,
|
||||
allowing users to publish generated blog content directly to their Wix site.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import tempfile
|
||||
import streamlit as st
|
||||
from typing import Dict, List, Optional, Union, Any, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
from .wix_integration import WixIntegration
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('wix_blog_publisher')
|
||||
|
||||
def publish_to_wix(
|
||||
title: str,
|
||||
content: str,
|
||||
is_markdown: bool = True,
|
||||
featured_image_path: Optional[str] = None,
|
||||
featured_image_url: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
author_name: Optional[str] = None,
|
||||
publisher_name: Optional[str] = None,
|
||||
publisher_logo_url: Optional[str] = None,
|
||||
publish: bool = True,
|
||||
update_if_exists: bool = True,
|
||||
api_key: Optional[str] = None,
|
||||
refresh_token: Optional[str] = None,
|
||||
site_id: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Publish a blog post to Wix.
|
||||
|
||||
Args:
|
||||
title: Post title
|
||||
content: Post content (markdown or HTML)
|
||||
is_markdown: Whether the content is in markdown format
|
||||
featured_image_path: Local path to featured image (optional)
|
||||
featured_image_url: URL of featured image to download (optional)
|
||||
excerpt: Post excerpt/summary (optional)
|
||||
tags: List of tags (optional)
|
||||
categories: List of category names (optional)
|
||||
seo_title: SEO title (optional)
|
||||
seo_description: SEO description (optional)
|
||||
seo_keywords: SEO keywords (optional)
|
||||
author_name: Name of the author (optional)
|
||||
publisher_name: Name of the publisher (optional)
|
||||
publisher_logo_url: URL of the publisher's logo (optional)
|
||||
publish: Whether to publish the post immediately (optional)
|
||||
update_if_exists: Whether to update an existing post with the same title (optional)
|
||||
api_key: Wix API key (optional if using refresh token)
|
||||
refresh_token: Wix refresh token for OAuth authentication
|
||||
site_id: Wix site ID
|
||||
|
||||
Returns:
|
||||
Published blog post data
|
||||
"""
|
||||
# Initialize Wix integration
|
||||
wix = WixIntegration(api_key, refresh_token, site_id)
|
||||
|
||||
# Publish the blog post
|
||||
return wix.publish_blog_post(
|
||||
title=title,
|
||||
content=content,
|
||||
is_markdown=is_markdown,
|
||||
featured_image_path=featured_image_path,
|
||||
featured_image_url=featured_image_url,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=categories,
|
||||
seo_title=seo_title,
|
||||
seo_description=seo_description,
|
||||
seo_keywords=seo_keywords,
|
||||
author_name=author_name,
|
||||
publisher_name=publisher_name,
|
||||
publisher_logo_url=publisher_logo_url,
|
||||
publish=publish,
|
||||
update_if_exists=update_if_exists
|
||||
)
|
||||
|
||||
def wix_blog_publisher_ui():
|
||||
"""
|
||||
Streamlit UI for publishing blog posts to Wix.
|
||||
"""
|
||||
st.title("Publish to Wix")
|
||||
st.write("Publish your blog content directly to your Wix site.")
|
||||
|
||||
# Authentication settings
|
||||
st.header("Wix Authentication")
|
||||
|
||||
# Check for saved credentials
|
||||
if "wix_refresh_token" in st.session_state and "wix_site_id" in st.session_state:
|
||||
st.success("✅ Wix credentials are saved in this session.")
|
||||
show_saved = st.checkbox("Show saved credentials")
|
||||
if show_saved:
|
||||
st.text_input("Refresh Token", value=st.session_state.wix_refresh_token, type="password", disabled=True)
|
||||
st.text_input("Site ID", value=st.session_state.wix_site_id, disabled=True)
|
||||
|
||||
clear_creds = st.button("Clear saved credentials")
|
||||
if clear_creds:
|
||||
if "wix_refresh_token" in st.session_state:
|
||||
del st.session_state.wix_refresh_token
|
||||
if "wix_site_id" in st.session_state:
|
||||
del st.session_state.wix_site_id
|
||||
st.rerun()
|
||||
else:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
refresh_token = st.text_input("Wix Refresh Token", type="password", help="Your Wix refresh token for API authentication")
|
||||
|
||||
with col2:
|
||||
site_id = st.text_input("Wix Site ID", help="Your Wix site ID")
|
||||
|
||||
save_creds = st.checkbox("Save credentials for this session", value=True)
|
||||
|
||||
if st.button("Validate Credentials"):
|
||||
if not refresh_token:
|
||||
st.error("Refresh token is required.")
|
||||
return
|
||||
|
||||
if not site_id:
|
||||
st.error("Site ID is required.")
|
||||
return
|
||||
|
||||
# Try to initialize Wix integration to validate credentials
|
||||
try:
|
||||
wix = WixIntegration(refresh_token=refresh_token, site_id=site_id)
|
||||
# Test API call
|
||||
site_info = wix.get_site_info()
|
||||
if site_info.get("status") == "connected":
|
||||
st.success(f"✅ Credentials validated successfully! Found {site_info.get('post_count', 0)} posts and {site_info.get('category_count', 0)} categories.")
|
||||
|
||||
# Save credentials if requested
|
||||
if save_creds:
|
||||
st.session_state.wix_refresh_token = refresh_token
|
||||
st.session_state.wix_site_id = site_id
|
||||
st.rerun()
|
||||
else:
|
||||
st.error(f"❌ Failed to validate credentials: {site_info.get('error', 'Unknown error')}")
|
||||
except Exception as e:
|
||||
st.error(f"❌ Failed to validate credentials: {str(e)}")
|
||||
return
|
||||
|
||||
# Blog content section
|
||||
st.header("Blog Content")
|
||||
|
||||
# Check if we have content in session state (from other parts of the app)
|
||||
blog_title = st.text_input(
|
||||
"Blog Title",
|
||||
value=st.session_state.get("blog_title", ""),
|
||||
help="The title of your blog post"
|
||||
)
|
||||
|
||||
content_type = st.radio(
|
||||
"Content Format",
|
||||
["Markdown", "HTML"],
|
||||
horizontal=True,
|
||||
help="The format of your blog content"
|
||||
)
|
||||
is_markdown = content_type == "Markdown"
|
||||
|
||||
blog_content = st.text_area(
|
||||
"Blog Content",
|
||||
value=st.session_state.get("blog_content", ""),
|
||||
height=300,
|
||||
help="The content of your blog post"
|
||||
)
|
||||
|
||||
# Featured image
|
||||
st.subheader("Featured Image")
|
||||
image_source = st.radio(
|
||||
"Image Source",
|
||||
["None", "Upload", "URL"],
|
||||
horizontal=True,
|
||||
help="How to provide the featured image"
|
||||
)
|
||||
|
||||
featured_image_path = None
|
||||
featured_image_url = None
|
||||
|
||||
if image_source == "Upload":
|
||||
uploaded_file = st.file_uploader("Upload Featured Image", type=["jpg", "jpeg", "png", "gif"])
|
||||
if uploaded_file:
|
||||
# Save the uploaded file to a temporary location
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{uploaded_file.name.split('.')[-1]}") as tmp:
|
||||
tmp.write(uploaded_file.getvalue())
|
||||
featured_image_path = tmp.name
|
||||
elif image_source == "URL":
|
||||
featured_image_url = st.text_input("Featured Image URL", help="URL of the featured image")
|
||||
|
||||
# Blog metadata
|
||||
st.header("Blog Metadata")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
excerpt = st.text_area(
|
||||
"Excerpt",
|
||||
value=st.session_state.get("blog_excerpt", ""),
|
||||
help="A short summary of your blog post"
|
||||
)
|
||||
|
||||
tags_input = st.text_input(
|
||||
"Tags (comma-separated)",
|
||||
value=", ".join(st.session_state.get("blog_tags", [])) if isinstance(st.session_state.get("blog_tags", []), list) else st.session_state.get("blog_tags", ""),
|
||||
help="Tags for your blog post, separated by commas"
|
||||
)
|
||||
tags = [tag.strip() for tag in tags_input.split(",")] if tags_input else None
|
||||
|
||||
categories_input = st.text_input(
|
||||
"Categories (comma-separated)",
|
||||
value=", ".join(st.session_state.get("blog_categories", [])) if isinstance(st.session_state.get("blog_categories", []), list) else st.session_state.get("blog_categories", ""),
|
||||
help="Categories for your blog post, separated by commas"
|
||||
)
|
||||
categories = [cat.strip() for cat in categories_input.split(",")] if categories_input else None
|
||||
|
||||
with col2:
|
||||
author_name = st.text_input("Author Name", help="Name of the blog post author")
|
||||
publisher_name = st.text_input("Publisher Name", help="Name of the blog publisher (usually your site name)")
|
||||
publisher_logo_url = st.text_input("Publisher Logo URL", help="URL of the publisher's logo")
|
||||
|
||||
# SEO settings
|
||||
with st.expander("SEO Settings"):
|
||||
seo_title = st.text_input("SEO Title", value=blog_title, help="Title for search engines (defaults to blog title)")
|
||||
seo_description = st.text_area("SEO Description", value=excerpt, help="Description for search engines (defaults to excerpt)")
|
||||
seo_keywords_input = st.text_input("SEO Keywords (comma-separated)", value=tags_input, help="Keywords for search engines (defaults to tags)")
|
||||
seo_keywords = [kw.strip() for kw in seo_keywords_input.split(",")] if seo_keywords_input else None
|
||||
|
||||
# Publishing options
|
||||
st.header("Publishing Options")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
publish = not st.checkbox("Save as draft", help="If checked, the post will be saved as a draft instead of being published")
|
||||
|
||||
with col2:
|
||||
update_if_exists = st.checkbox("Update if exists", value=True, help="If checked, an existing post with the same title will be updated")
|
||||
|
||||
# Publish button
|
||||
if st.button("Publish to Wix", type="primary"):
|
||||
if not blog_title:
|
||||
st.error("Blog title is required.")
|
||||
return
|
||||
|
||||
if not blog_content:
|
||||
st.error("Blog content is required.")
|
||||
return
|
||||
|
||||
# Get credentials
|
||||
refresh_token = st.session_state.get("wix_refresh_token")
|
||||
site_id = st.session_state.get("wix_site_id")
|
||||
|
||||
if not refresh_token or not site_id:
|
||||
st.error("Wix credentials are required. Please enter them in the authentication section.")
|
||||
return
|
||||
|
||||
# Show progress
|
||||
with st.spinner("Publishing to Wix..."):
|
||||
try:
|
||||
# Publish to Wix
|
||||
result = publish_to_wix(
|
||||
title=blog_title,
|
||||
content=blog_content,
|
||||
is_markdown=is_markdown,
|
||||
featured_image_path=featured_image_path,
|
||||
featured_image_url=featured_image_url,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=categories,
|
||||
seo_title=seo_title,
|
||||
seo_description=seo_description,
|
||||
seo_keywords=seo_keywords,
|
||||
author_name=author_name,
|
||||
publisher_name=publisher_name,
|
||||
publisher_logo_url=publisher_logo_url,
|
||||
publish=publish,
|
||||
update_if_exists=update_if_exists,
|
||||
refresh_token=refresh_token,
|
||||
site_id=site_id
|
||||
)
|
||||
|
||||
# Clean up temporary file if created
|
||||
if featured_image_path and os.path.exists(featured_image_path) and featured_image_path.startswith(tempfile.gettempdir()):
|
||||
try:
|
||||
os.remove(featured_image_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Show success message
|
||||
st.success("✅ Blog post published successfully!")
|
||||
|
||||
# Show post details
|
||||
post = result.get("post", {})
|
||||
st.subheader("Published Post Details")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.write(f"**Title:** {post.get('title', 'N/A')}")
|
||||
st.write(f"**Status:** {post.get('status', 'N/A')}")
|
||||
st.write(f"**ID:** {post.get('id', 'N/A')}")
|
||||
|
||||
with col2:
|
||||
st.write(f"**Published Date:** {post.get('publishedDate', 'N/A')}")
|
||||
st.write(f"**URL:** {post.get('url', 'N/A')}")
|
||||
st.write(f"**Tags:** {', '.join(post.get('tags', []))}")
|
||||
|
||||
# Add a view button if URL is available
|
||||
if post.get("url"):
|
||||
st.markdown(f"[View Post]({post.get('url')})")
|
||||
|
||||
# Add SEO report button
|
||||
if st.button("Generate SEO Report"):
|
||||
with st.spinner("Generating SEO report..."):
|
||||
try:
|
||||
wix = WixIntegration(refresh_token=refresh_token, site_id=site_id)
|
||||
seo_report = wix.get_seo_report(post.get("id"), seo_keywords or tags or [])
|
||||
|
||||
st.subheader("SEO Report")
|
||||
st.write(f"**SEO Score:** {seo_report.get('seo_score', 0):.1f}/100")
|
||||
|
||||
st.write("**Recommendations:**")
|
||||
for i, rec in enumerate(seo_report.get("recommendations", [])):
|
||||
st.write(f"{i+1}. {rec}")
|
||||
except Exception as e:
|
||||
st.error(f"Failed to generate SEO report: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ Failed to publish blog post: {str(e)}")
|
||||
logger.error(f"Failed to publish blog post: {str(e)}")
|
||||
|
||||
# For testing the UI directly
|
||||
if __name__ == "__main__":
|
||||
wix_blog_publisher_ui()
|
||||
@@ -1,388 +0,0 @@
|
||||
"""
|
||||
Wix Integration for Alwrity
|
||||
|
||||
This module provides a high-level interface for integrating Wix blog functionality
|
||||
with the Alwrity AI Writer platform.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
from typing import Dict, List, Optional, Union, Any, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
from .wix_api_client import WixAPIClient
|
||||
from .wix_blog_manager import WixBlogManager
|
||||
from .wix_seo_optimizer import WixSEOOptimizer
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('wix_integration')
|
||||
|
||||
class WixIntegration:
|
||||
"""
|
||||
Main integration class for Wix blog functionality.
|
||||
|
||||
This class provides a simplified interface for common operations,
|
||||
combining the functionality of the API client, blog manager, and SEO optimizer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: Optional[str] = None,
|
||||
refresh_token: Optional[str] = None,
|
||||
site_id: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Initialize the Wix Integration.
|
||||
|
||||
Args:
|
||||
api_key: Wix API key (optional if using refresh token)
|
||||
refresh_token: Wix refresh token for OAuth authentication
|
||||
site_id: Wix site ID
|
||||
"""
|
||||
self.api_client = WixAPIClient(api_key, refresh_token, site_id)
|
||||
self.blog_manager = WixBlogManager(api_key, refresh_token, site_id)
|
||||
self.seo_optimizer = WixSEOOptimizer(api_key, refresh_token, site_id)
|
||||
|
||||
def publish_blog_post(
|
||||
self,
|
||||
title: str,
|
||||
content: str,
|
||||
is_markdown: bool = True,
|
||||
featured_image_path: Optional[str] = None,
|
||||
featured_image_url: Optional[str] = None,
|
||||
excerpt: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
categories: Optional[List[str]] = None,
|
||||
seo_title: Optional[str] = None,
|
||||
seo_description: Optional[str] = None,
|
||||
seo_keywords: Optional[List[str]] = None,
|
||||
author_name: Optional[str] = None,
|
||||
publisher_name: Optional[str] = None,
|
||||
publisher_logo_url: Optional[str] = None,
|
||||
publish: bool = True,
|
||||
update_if_exists: bool = True
|
||||
) -> Dict:
|
||||
"""
|
||||
Publish a blog post with comprehensive SEO optimization.
|
||||
|
||||
Args:
|
||||
title: Post title
|
||||
content: Post content (markdown or HTML)
|
||||
is_markdown: Whether the content is in markdown format
|
||||
featured_image_path: Local path to featured image (optional)
|
||||
featured_image_url: URL of featured image to download (optional)
|
||||
excerpt: Post excerpt/summary (optional)
|
||||
tags: List of tags (optional)
|
||||
categories: List of category names (optional)
|
||||
seo_title: SEO title (optional)
|
||||
seo_description: SEO description (optional)
|
||||
seo_keywords: SEO keywords (optional)
|
||||
author_name: Name of the author (optional)
|
||||
publisher_name: Name of the publisher (optional)
|
||||
publisher_logo_url: URL of the publisher's logo (optional)
|
||||
publish: Whether to publish the post immediately (optional)
|
||||
update_if_exists: Whether to update an existing post with the same title (optional)
|
||||
|
||||
Returns:
|
||||
Published blog post data
|
||||
"""
|
||||
# Generate SEO data if not provided
|
||||
if not seo_keywords and tags:
|
||||
seo_keywords = tags
|
||||
|
||||
if not seo_title:
|
||||
seo_title = title
|
||||
|
||||
if not seo_description and not excerpt:
|
||||
if is_markdown:
|
||||
# Generate description from markdown content
|
||||
seo_description = self.blog_manager._generate_excerpt(content)
|
||||
else:
|
||||
# Generate description from HTML content
|
||||
seo_description = self.seo_optimizer.generate_meta_description(content)
|
||||
elif not seo_description:
|
||||
seo_description = excerpt
|
||||
|
||||
# Publish or update the post
|
||||
if is_markdown:
|
||||
response = self.blog_manager.publish_or_update_markdown_post(
|
||||
title=title,
|
||||
markdown_content=content,
|
||||
featured_image_path=featured_image_path,
|
||||
featured_image_url=featured_image_url,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=categories,
|
||||
seo_title=seo_title,
|
||||
seo_description=seo_description,
|
||||
seo_keywords=seo_keywords,
|
||||
publish=publish,
|
||||
update_if_exists=update_if_exists
|
||||
)
|
||||
else:
|
||||
# Find existing post or create new one
|
||||
existing_post = self.blog_manager.find_post_by_title(title)
|
||||
|
||||
if existing_post and update_if_exists:
|
||||
# Update existing post
|
||||
response = self.api_client.update_post(
|
||||
post_id=existing_post["id"],
|
||||
title=title,
|
||||
content=content,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=[self.api_client.get_or_create_category(cat) for cat in categories] if categories else None,
|
||||
seo_data={
|
||||
"title": seo_title,
|
||||
"description": seo_description,
|
||||
"keywords": seo_keywords or []
|
||||
},
|
||||
publish=publish
|
||||
)
|
||||
else:
|
||||
# Create new post
|
||||
response = self.api_client.create_post(
|
||||
title=title,
|
||||
content=content,
|
||||
excerpt=excerpt,
|
||||
tags=tags,
|
||||
categories=[self.api_client.get_or_create_category(cat) for cat in categories] if categories else None,
|
||||
seo_data={
|
||||
"title": seo_title,
|
||||
"description": seo_description,
|
||||
"keywords": seo_keywords or []
|
||||
},
|
||||
publish=publish
|
||||
)
|
||||
|
||||
# Apply additional SEO optimization if the post was published
|
||||
if publish and response.get("post", {}).get("id"):
|
||||
post_id = response["post"]["id"]
|
||||
|
||||
# Apply structured data if author and publisher info is provided
|
||||
if author_name and publisher_name and publisher_logo_url:
|
||||
try:
|
||||
self.seo_optimizer.apply_structured_data_to_post(
|
||||
post_id=post_id,
|
||||
author_name=author_name,
|
||||
publisher_name=publisher_name,
|
||||
publisher_logo_url=publisher_logo_url
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply structured data: {str(e)}")
|
||||
|
||||
# Apply comprehensive SEO optimization
|
||||
try:
|
||||
self.seo_optimizer.apply_seo_optimization(
|
||||
post_id=post_id,
|
||||
title=seo_title,
|
||||
description=seo_description,
|
||||
keywords=seo_keywords,
|
||||
author_name=author_name,
|
||||
publisher_name=publisher_name,
|
||||
publisher_logo_url=publisher_logo_url,
|
||||
og_image_url=featured_image_url
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply SEO optimization: {str(e)}")
|
||||
|
||||
return response
|
||||
|
||||
def upload_media(
|
||||
self,
|
||||
file_path: str,
|
||||
title: Optional[str] = None,
|
||||
alt_text: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Upload a media file to Wix.
|
||||
|
||||
Args:
|
||||
file_path: Path to the media file
|
||||
title: Media title (optional)
|
||||
alt_text: Media alt text (optional)
|
||||
description: Media description (optional)
|
||||
|
||||
Returns:
|
||||
Uploaded media data
|
||||
"""
|
||||
return self.api_client.upload_image(
|
||||
file_path=file_path,
|
||||
title=title,
|
||||
alt_text=alt_text,
|
||||
description=description
|
||||
)
|
||||
|
||||
def get_seo_report(self, post_id: str, target_keywords: List[str]) -> Dict:
|
||||
"""
|
||||
Generate a comprehensive SEO report for a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
target_keywords: List of target keywords
|
||||
|
||||
Returns:
|
||||
Dictionary with SEO report data
|
||||
"""
|
||||
return self.seo_optimizer.generate_seo_report(post_id, target_keywords)
|
||||
|
||||
def list_blog_posts(
|
||||
self,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
sort_field: str = "lastPublishedDate",
|
||||
sort_order: str = "desc"
|
||||
) -> Dict:
|
||||
"""
|
||||
List blog posts with pagination and sorting.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of posts to return (default: 50)
|
||||
offset: Pagination offset (default: 0)
|
||||
sort_field: Field to sort by (default: lastPublishedDate)
|
||||
sort_order: Sort order, 'asc' or 'desc' (default: desc)
|
||||
|
||||
Returns:
|
||||
Dictionary containing blog posts and pagination info
|
||||
"""
|
||||
return self.api_client.list_posts(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
def list_categories(self) -> Dict:
|
||||
"""
|
||||
List all blog categories.
|
||||
|
||||
Returns:
|
||||
Dictionary containing blog categories
|
||||
"""
|
||||
return self.api_client.list_categories()
|
||||
|
||||
def create_category(self, name: str, description: Optional[str] = None) -> str:
|
||||
"""
|
||||
Create a new blog category.
|
||||
|
||||
Args:
|
||||
name: Category name
|
||||
description: Category description (optional)
|
||||
|
||||
Returns:
|
||||
ID of the created category
|
||||
"""
|
||||
response = self.api_client.create_category(
|
||||
label=name,
|
||||
description=description
|
||||
)
|
||||
return response.get("category", {}).get("id", "")
|
||||
|
||||
def get_post_by_id(self, post_id: str) -> Dict:
|
||||
"""
|
||||
Get a blog post by ID.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
|
||||
Returns:
|
||||
Blog post data
|
||||
"""
|
||||
return self.api_client.get_post(post_id)
|
||||
|
||||
def get_post_by_title(self, title: str) -> Optional[Dict]:
|
||||
"""
|
||||
Get a blog post by title.
|
||||
|
||||
Args:
|
||||
title: Title of the blog post
|
||||
|
||||
Returns:
|
||||
Blog post data or None if not found
|
||||
"""
|
||||
return self.blog_manager.find_post_by_title(title)
|
||||
|
||||
def delete_post(self, post_id: str) -> Dict:
|
||||
"""
|
||||
Delete a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
|
||||
Returns:
|
||||
Response data
|
||||
"""
|
||||
return self.api_client.delete_post(post_id)
|
||||
|
||||
def update_post_status(self, post_id: str, publish: bool = True) -> Dict:
|
||||
"""
|
||||
Update the publication status of a blog post.
|
||||
|
||||
Args:
|
||||
post_id: ID of the blog post
|
||||
publish: Whether to publish (True) or unpublish (False) the post
|
||||
|
||||
Returns:
|
||||
Updated blog post data
|
||||
"""
|
||||
if publish:
|
||||
return self.api_client.publish_post(post_id)
|
||||
else:
|
||||
return self.api_client.unpublish_post(post_id)
|
||||
|
||||
def search_posts(self, query: str, limit: int = 10) -> List[Dict]:
|
||||
"""
|
||||
Search for blog posts by content or title.
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
limit: Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
List of matching blog posts
|
||||
"""
|
||||
# First try to find by title
|
||||
title_matches = []
|
||||
try:
|
||||
all_posts = self.list_blog_posts(limit=100)["posts"]
|
||||
for post in all_posts:
|
||||
if query.lower() in post.get("title", "").lower():
|
||||
title_matches.append(post)
|
||||
if len(title_matches) >= limit:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching posts by title: {str(e)}")
|
||||
|
||||
return title_matches[:limit]
|
||||
|
||||
def get_site_info(self) -> Dict:
|
||||
"""
|
||||
Get information about the Wix site.
|
||||
|
||||
Returns:
|
||||
Dictionary with site information
|
||||
"""
|
||||
try:
|
||||
# Make a simple API call to verify credentials and get site info
|
||||
posts = self.list_blog_posts(limit=1)
|
||||
categories = self.list_categories()
|
||||
|
||||
return {
|
||||
"site_id": self.api_client.site_id,
|
||||
"post_count": posts.get("totalCount", 0),
|
||||
"category_count": len(categories.get("categories", [])),
|
||||
"status": "connected"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting site info: {str(e)}")
|
||||
return {
|
||||
"site_id": self.api_client.site_id,
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import mimetypes
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
import base64
|
||||
import json
|
||||
from clint.textui import progress
|
||||
|
||||
from PIL import Image
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
## Check if blog needs to be posted on wordpress.
|
||||
#if wordpress:
|
||||
## Fixme: Fetch all tags and categories to check, if present ones are present and
|
||||
## use them else create new ones. Its better to use chatgpt than string comparison.
|
||||
## Similar tags and categories will be missed.
|
||||
## blog_categories =
|
||||
## blog_tags =
|
||||
#logger.info("Uploading the blog to wordpress.\n")
|
||||
#main_img_path = compress_image(main_img_path, quality=85)
|
||||
#try:
|
||||
# img_details = analyze_and_extract_details_from_image(main_img_path)
|
||||
# alt_text = img_details.get('alt_text')
|
||||
# img_description = img_details.get('description')
|
||||
# img_title = img_details.get('title')
|
||||
# caption = img_details.get('caption')
|
||||
# try:
|
||||
# media = upload_media(wordpress_url, wordpress_username, wordpress_password,
|
||||
# main_img_path, alt_text, img_description, img_title, caption)
|
||||
# except Exception as err:
|
||||
# sys.exit(f"Error occurred in upload_media: {err}")
|
||||
#except Exception as e:
|
||||
# sys.exit(f"Error occurred in analyze_and_extract_details_from_image: {e}")
|
||||
#
|
||||
## Then create the post with the uploaded media as the featured image
|
||||
#media_id = media['id']
|
||||
#blog_markdown_str = convert_markdown_to_html(blog_markdown_str)
|
||||
#try:
|
||||
# upload_blog_post(wordpress_url, wordpress_username, wordpress_password, a_blog_topic,
|
||||
# blog_markdown_str, media_id, blog_meta_desc, blog_categories, blog_tags, status='publish')
|
||||
#except Exception as err:
|
||||
# sys.exit(f"Failed to upload blog to wordpress.Error: {err}")
|
||||
|
||||
|
||||
def compress_image(image_path, quality=85):
|
||||
"""
|
||||
Compress the image by reducing its quality and logger.info size information.
|
||||
|
||||
:param image_path: Path to the original image
|
||||
:param quality: Quality of the output image (1-100), lower means more compression
|
||||
:return: Path to the compressed image
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
raise ValueError(f"Provided image path does not exist: {image_path}")
|
||||
|
||||
# Get the size of the original image
|
||||
original_size = os.path.getsize(image_path)
|
||||
|
||||
# Open the image
|
||||
with Image.open(image_path) as img:
|
||||
# Define the format based on the original image format
|
||||
img_format = img.format
|
||||
|
||||
# Create a temporary file to save the compressed image
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.' + img_format.lower())
|
||||
|
||||
# Save the image with reduced quality
|
||||
img.save(temp_file, format=img_format, quality=quality, optimize=True)
|
||||
|
||||
# Get the size of the compressed image
|
||||
compressed_size = os.path.getsize(temp_file.name)
|
||||
|
||||
# Calculate the percentage reduction
|
||||
reduction = (1 - (compressed_size / original_size)) * 100
|
||||
logger.info("########### Image Compression ###############")
|
||||
logger.info(f"Compressing the image, Original size: {original_size / 1024:.2f} KB")
|
||||
logger.info(f"Compressed size: {compressed_size / 1024:.2f} KB")
|
||||
logger.info(f"Reduction in image size: {reduction:.2f}%")
|
||||
# TBD: https://tinypng.com/developers/reference/python
|
||||
logger.info(f"Note: Consider converting images to JPEG/WebP format.\n\n")
|
||||
|
||||
return temp_file.name
|
||||
|
||||
|
||||
def create_wordpress_tag(url, username, app_password, tag_name):
|
||||
"""
|
||||
Create a new tag in WordPress using the REST API and return its ID.
|
||||
|
||||
:param url: URL of the WordPress site (e.g., 'https://example.com')
|
||||
:param username: WordPress username
|
||||
:param app_password: WordPress application password
|
||||
:param tag_name: Name of the tag to be created
|
||||
:return: ID of the created tag or error message
|
||||
"""
|
||||
api_endpoint = f"{url}/wp-json/wp/v2/tags"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
data = {
|
||||
'name': tag_name,
|
||||
}
|
||||
response = requests.post(api_endpoint, json=data, auth=HTTPBasicAuth(username, app_password), headers=headers)
|
||||
|
||||
if response.status_code == 201:
|
||||
return response.json().get('id') # Return the ID of the created tag
|
||||
else:
|
||||
return response.text
|
||||
|
||||
|
||||
def create_wordpress_category(url, username, app_password, category_name):
|
||||
"""
|
||||
Create a new category in WordPress using the REST API and return its ID.
|
||||
|
||||
:param url: URL of the WordPress site (e.g., 'https://example.com')
|
||||
:param username: WordPress username
|
||||
:param app_password: WordPress application password
|
||||
:param category_name: Name of the category to be created
|
||||
:return: ID of the created category or error message
|
||||
"""
|
||||
api_endpoint = f"{url}/wp-json/wp/v2/categories"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
data = {
|
||||
'name': category_name,
|
||||
}
|
||||
response = requests.post(api_endpoint, json=data, auth=HTTPBasicAuth(username, app_password), headers=headers)
|
||||
|
||||
if response.status_code == 201:
|
||||
return response.json().get('id') # Return the ID of the created category
|
||||
else:
|
||||
return response.text
|
||||
|
||||
|
||||
def get_all_wordpress_categories(url, username, password):
|
||||
"""
|
||||
Get all categories from WordPress.
|
||||
|
||||
:param url: URL of the WordPress site
|
||||
:param username: WordPress username
|
||||
:param password: WordPress application password
|
||||
:return: Dictionary of category names and their IDs
|
||||
"""
|
||||
logger.info("Fetching all wordpress categories to create Or use exsiting.")
|
||||
categories = {}
|
||||
api_endpoint = f"{url}/wp-json/wp/v2/categories"
|
||||
response = requests.get(api_endpoint, auth=HTTPBasicAuth(username, password))
|
||||
|
||||
if response.status_code == 200:
|
||||
for category in response.json():
|
||||
categories[category['name']] = category['id']
|
||||
return categories
|
||||
else:
|
||||
return "Error: " + response.text
|
||||
|
||||
|
||||
def get_all_wordpress_tags(url, username, password):
|
||||
"""
|
||||
Get all tags from WordPress.
|
||||
|
||||
:param url: URL of the WordPress site
|
||||
:param username: WordPress username
|
||||
:param password: WordPress application password
|
||||
:return: Dictionary of tag names and their IDs
|
||||
"""
|
||||
logger.info("Fetching all tags from wordpress to create or use existing tag.")
|
||||
tags = {}
|
||||
api_endpoint = f"{url}/wp-json/wp/v2/tags"
|
||||
response = requests.get(api_endpoint, auth=HTTPBasicAuth(username, password))
|
||||
|
||||
if response.status_code == 200:
|
||||
for tag in response.json():
|
||||
tags[tag['name']] = tag['id']
|
||||
return tags
|
||||
else:
|
||||
return "Error: " + response.text
|
||||
|
||||
|
||||
def create_or_get_wordpress_category(url, username, password, category_name):
|
||||
"""
|
||||
Create a new category or get existing one from WordPress.
|
||||
|
||||
:param url: URL of the WordPress site
|
||||
:param username: WordPress username
|
||||
:param password: WordPress application password
|
||||
:param category_name: Name of the category
|
||||
:return: ID of the category
|
||||
"""
|
||||
existing_categories = get_all_wordpress_categories(url, username, password)
|
||||
if category_name in existing_categories:
|
||||
return existing_categories[category_name]
|
||||
else:
|
||||
return create_wordpress_category(url, username, password, category_name)
|
||||
|
||||
|
||||
def create_or_get_wordpress_tag(url, username, password, tag_name):
|
||||
"""
|
||||
Create a new tag or get existing one from WordPress.
|
||||
|
||||
:param url: URL of the WordPress site
|
||||
:param username: WordPress username
|
||||
:param password: WordPress application password
|
||||
:param tag_name: Name of the tag
|
||||
:return: ID of the tag
|
||||
"""
|
||||
existing_tags = get_all_wordpress_tags(url, username, password)
|
||||
if tag_name in existing_tags:
|
||||
return existing_tags[tag_name]
|
||||
else:
|
||||
return create_wordpress_tag(url, username, password, tag_name)
|
||||
|
||||
|
||||
def upload_media(url, username, password, media_path, alt_text, description, title, caption):
|
||||
"""
|
||||
Upload media to WordPress site with alt text, description, title, and caption.
|
||||
|
||||
:param url: URL of your WordPress site
|
||||
:param username: Your WordPress username
|
||||
:param password: Your WordPress password
|
||||
:param media_path: Path to the media file
|
||||
:param alt_text: Alternative text for the image
|
||||
:param description: Description of the media
|
||||
:param title: Title of the media
|
||||
:param caption: Caption for the media
|
||||
"""
|
||||
if not os.path.exists(media_path):
|
||||
logger.info(f"File not found: {media_path}")
|
||||
return None
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(media_path)
|
||||
if mime_type is None:
|
||||
logger.info(f"Unable to determine MIME type for the file: {media_path}")
|
||||
return None
|
||||
|
||||
credentials = username + ':' + password
|
||||
token = base64.b64encode(credentials.encode())
|
||||
header = {
|
||||
'Authorization': 'Basic ' + token.decode('utf-8'),
|
||||
'Content-Disposition': 'attachment; filename={}'.format(os.path.basename(media_path))
|
||||
}
|
||||
|
||||
with open(media_path, 'rb', encoding="utf-8") as media:
|
||||
media_name = os.path.basename(media_path)
|
||||
files = {'file': (media_name, media, mime_type)}
|
||||
|
||||
# Upload the media file
|
||||
response = requests.post(url + '/wp-json/wp/v2/media', headers=header, files=files)
|
||||
|
||||
if response.status_code == 201:
|
||||
logger.info("Media uploaded successfully.")
|
||||
media_id = response.json()['id']
|
||||
|
||||
# Update media with alt text, description, title, and caption
|
||||
media_data = {
|
||||
'alt_text': alt_text,
|
||||
'description': description,
|
||||
'title': title,
|
||||
'caption': caption
|
||||
}
|
||||
|
||||
media_update_response = requests.post(f"{url}/wp-json/wp/v2/media/{media_id}", headers=header, json=media_data)
|
||||
|
||||
if media_update_response.status_code == 200:
|
||||
logger.info("Media updated with alt text, description, title, and caption successfully.")
|
||||
return media_update_response.json()
|
||||
else:
|
||||
logger.error("Failed to update media.")
|
||||
logger.error(f"Response:{media_update_response.content}")
|
||||
return None
|
||||
else:
|
||||
logger.error("Failed to upload media.")
|
||||
logger.error("Response:{response.content}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def upload_blog_post(url, username, password, title, content, media_id, meta_desc, categories=None, tags=None, status='draft'):
|
||||
"""
|
||||
Upload a blog post to a WordPress site.
|
||||
https://developer.wordpress.org/rest-api/reference/posts/#create-a-post
|
||||
|
||||
:param url: URL of your WordPress site
|
||||
:param username: Your WordPress username
|
||||
:param password: Your WordPress password
|
||||
:param title: Title of the blog post
|
||||
:param content: Content of the blog post
|
||||
:param media_id: ID of the uploaded media to be set as the featured image
|
||||
:param categories: List of category IDs
|
||||
:param tags: List of tag IDs
|
||||
:param status: Status of the post ('draft', 'publish', etc.)
|
||||
"""
|
||||
credentials = username + ':' + password
|
||||
token = base64.b64encode(credentials.encode())
|
||||
header = {'Authorization': 'Basic ' + token.decode('utf-8')}
|
||||
|
||||
# Prepare the data for the post
|
||||
# https://developer.wordpress.org/rest-api/reference/posts/#schema-meta
|
||||
post = {
|
||||
'title': title,
|
||||
'content': content,
|
||||
# One of: publish, future, draft, pending, private
|
||||
'status': status,
|
||||
'excerpt': meta_desc,
|
||||
'featured_media': media_id,
|
||||
#'categories': categories,
|
||||
#'tags': tags,
|
||||
|
||||
'meta': {
|
||||
'description': meta_desc # This depends on your WordPress setup
|
||||
}
|
||||
}
|
||||
#if categories:
|
||||
# post['categories'] = categories
|
||||
|
||||
# Make the request
|
||||
response = requests.post(url + '/wp-json/wp/v2/posts', headers=header, json=post)
|
||||
|
||||
# Check response
|
||||
if response.status_code == 201:
|
||||
logger.info("Blog to wordpress, uploaded successfully.")
|
||||
return json.loads(response.content)
|
||||
else:
|
||||
logger.error("Blog upload to wordpress Failed.")
|
||||
logger.error(f"Response: {response.content}") # Print response content for debugging
|
||||
return None
|
||||
Reference in New Issue
Block a user