Added new features to the project
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import os
|
||||
import configparser
|
||||
import streamlit as st
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai_tools import SerperDevTool
|
||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
|
||||
# Initialize session state variables if not already done
|
||||
@@ -12,6 +10,11 @@ if 'progress' not in st.session_state:
|
||||
|
||||
def create_agents(search_keywords):
|
||||
"""Create agents for content creation."""
|
||||
try:
|
||||
from crewai import Agent
|
||||
from crewai_tools import SerperDevTool
|
||||
except ImportError:
|
||||
raise ImportError("The 'crewai' and/or 'crewai_tools' package is not installed. Please install them to use AI Agents Crew Writer features.")
|
||||
search_tool = SerperDevTool()
|
||||
google_api_key = os.getenv("GEMINI_API_KEY")
|
||||
|
||||
@@ -52,6 +55,10 @@ def create_agents(search_keywords):
|
||||
|
||||
def create_tasks(agents, search_keywords):
|
||||
"""Create tasks for the agents."""
|
||||
try:
|
||||
from crewai import Task
|
||||
except ImportError:
|
||||
raise ImportError("The 'crewai' package is not installed. Please install it to use AI Agents Crew Writer features.")
|
||||
try:
|
||||
task_description, expected_output = read_config("research_task")
|
||||
research_task = Task(
|
||||
@@ -89,6 +96,10 @@ def create_tasks(agents, search_keywords):
|
||||
|
||||
def execute_tasks(agents, tasks, lang):
|
||||
"""Execute tasks with the agents."""
|
||||
try:
|
||||
from crewai import Crew
|
||||
except ImportError:
|
||||
raise ImportError("The 'crewai' package is not installed. Please install it to use AI Agents Crew Writer features.")
|
||||
crew = Crew(
|
||||
agents=agents,
|
||||
tasks=tasks,
|
||||
|
||||
@@ -12,9 +12,64 @@ from lib.ai_writers.ai_outline_writer.outline_ui import main as outline_generato
|
||||
from lib.alwrity_ui.dashboard_styles import apply_dashboard_style, render_dashboard_header, render_category_header, render_card
|
||||
from loguru import logger
|
||||
|
||||
# Try to import AI Content Performance Predictor (AI-first approach)
|
||||
try:
|
||||
from lib.content_performance_predictor.ai_performance_predictor import render_ai_predictor_ui as render_content_performance_predictor
|
||||
AI_PREDICTOR_AVAILABLE = True
|
||||
logger.info("AI Content Performance Predictor loaded successfully")
|
||||
except ImportError:
|
||||
logger.warning("AI Content Performance Predictor not available")
|
||||
render_content_performance_predictor = None
|
||||
AI_PREDICTOR_AVAILABLE = False
|
||||
|
||||
# Try to import Bootstrap AI Competitive Suite
|
||||
try:
|
||||
from lib.ai_competitive_suite.bootstrap_ai_suite import render_bootstrap_ai_suite
|
||||
BOOTSTRAP_SUITE_AVAILABLE = True
|
||||
logger.info("Bootstrap AI Competitive Suite loaded successfully")
|
||||
except ImportError:
|
||||
logger.warning("Bootstrap AI Competitive Suite not available")
|
||||
render_bootstrap_ai_suite = None
|
||||
BOOTSTRAP_SUITE_AVAILABLE = False
|
||||
|
||||
def list_ai_writers():
|
||||
"""Return a list of available AI writers with their metadata (no UI rendering)."""
|
||||
return [
|
||||
writers = []
|
||||
|
||||
# Add Content Performance Predictor if available
|
||||
if render_content_performance_predictor:
|
||||
# AI-first approach description
|
||||
if AI_PREDICTOR_AVAILABLE:
|
||||
description = "🎯 AI-powered content performance prediction with competitive intelligence - perfect for solo entrepreneurs"
|
||||
name = "AI Content Performance Predictor"
|
||||
else:
|
||||
description = "Predict content success before publishing with AI-powered performance analysis"
|
||||
name = "Content Performance Predictor"
|
||||
|
||||
writers.append({
|
||||
"name": name,
|
||||
"icon": "🎯",
|
||||
"description": description,
|
||||
"category": "⭐ Featured",
|
||||
"function": render_content_performance_predictor,
|
||||
"path": "performance_predictor",
|
||||
"featured": True
|
||||
})
|
||||
|
||||
# Add Bootstrap AI Competitive Suite if available
|
||||
if render_bootstrap_ai_suite:
|
||||
writers.append({
|
||||
"name": "Bootstrap AI Competitive Suite",
|
||||
"icon": "🚀",
|
||||
"description": "🥷 Complete AI-powered competitive toolkit: content performance prediction + competitive intelligence for solo entrepreneurs",
|
||||
"category": "⭐ Featured",
|
||||
"function": render_bootstrap_ai_suite,
|
||||
"path": "bootstrap_ai_suite",
|
||||
"featured": True
|
||||
})
|
||||
|
||||
# Add existing writers
|
||||
writers.extend([
|
||||
{
|
||||
"name": "AI Blog Writer",
|
||||
"icon": "📝",
|
||||
@@ -103,7 +158,9 @@ def list_ai_writers():
|
||||
"function": outline_generator,
|
||||
"path": "outline_generator"
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
return writers
|
||||
|
||||
def get_ai_writers():
|
||||
"""Main function to display AI writers dashboard with premium glassmorphic design."""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,22 @@
|
||||
"""
|
||||
Twitter Dashboard with modern UI components.
|
||||
Enhanced Twitter Dashboard with modern UI components and improved user experience.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional, Any
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from .tweet_generator import smart_tweet_generator
|
||||
from .twitter_streamlit_ui import (
|
||||
TwitterDashboard,
|
||||
FeatureCard,
|
||||
TweetCard,
|
||||
TweetForm,
|
||||
SettingsForm,
|
||||
Sidebar,
|
||||
@@ -22,290 +28,702 @@ from .twitter_streamlit_ui import (
|
||||
get_from_session,
|
||||
clear_session,
|
||||
show_success_message,
|
||||
show_error_message
|
||||
show_error_message,
|
||||
show_info_message,
|
||||
show_warning_message
|
||||
)
|
||||
|
||||
def load_feature_data() -> Dict:
|
||||
"""Load feature data from a structured format."""
|
||||
return {
|
||||
"tweet_generation": {
|
||||
"title": "Tweet Generation & Optimization",
|
||||
"icon": "🐦",
|
||||
"description": "Create and optimize engaging tweets with AI assistance",
|
||||
"features": [
|
||||
{
|
||||
"name": "Smart Tweet Generator",
|
||||
"description": "Generate multiple tweet variations with optimal character count, hashtags, and emojis",
|
||||
"status": "active",
|
||||
"icon": "✨",
|
||||
"function": smart_tweet_generator
|
||||
},
|
||||
{
|
||||
"name": "Tweet Performance Predictor",
|
||||
"description": "Predict engagement rates and best posting times for maximum impact",
|
||||
"status": "coming_soon",
|
||||
"icon": "📊"
|
||||
}
|
||||
]
|
||||
},
|
||||
"content_strategy": {
|
||||
"title": "Content Strategy Tools",
|
||||
"icon": "📅",
|
||||
"description": "Plan and manage your Twitter content strategy effectively",
|
||||
"features": [
|
||||
{
|
||||
"name": "Content Calendar Generator",
|
||||
"description": "Create weekly/monthly content plans with theme-based scheduling",
|
||||
"status": "coming_soon",
|
||||
"icon": "🗓️"
|
||||
},
|
||||
{
|
||||
"name": "Hashtag Strategy Manager",
|
||||
"description": "Research and manage trending hashtags for better reach",
|
||||
"status": "coming_soon",
|
||||
"icon": "#️⃣"
|
||||
}
|
||||
]
|
||||
},
|
||||
"visual_content": {
|
||||
"title": "Visual Content Creation",
|
||||
"icon": "🎨",
|
||||
"description": "Create engaging visual content for your tweets",
|
||||
"features": [
|
||||
{
|
||||
"name": "Image Generator",
|
||||
"description": "Create tweet cards, infographics, and quote designs",
|
||||
"status": "coming_soon",
|
||||
"icon": "🖼️"
|
||||
},
|
||||
{
|
||||
"name": "Video Content Assistant",
|
||||
"description": "Generate video scripts and optimize captions",
|
||||
"status": "coming_soon",
|
||||
"icon": "🎥"
|
||||
}
|
||||
]
|
||||
},
|
||||
"engagement": {
|
||||
"title": "Engagement & Community",
|
||||
"icon": "🤝",
|
||||
"description": "Manage and enhance community engagement",
|
||||
"features": [
|
||||
{
|
||||
"name": "Reply Generator",
|
||||
"description": "Generate context-aware responses with appropriate tone",
|
||||
"status": "coming_soon",
|
||||
"icon": "💬"
|
||||
},
|
||||
{
|
||||
"name": "Community Tools",
|
||||
"description": "Create polls and plan Q&A sessions",
|
||||
"status": "coming_soon",
|
||||
"icon": "👥"
|
||||
}
|
||||
]
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics & Optimization",
|
||||
"icon": "📈",
|
||||
"description": "Track performance and optimize your Twitter strategy",
|
||||
"features": [
|
||||
{
|
||||
"name": "Performance Analytics",
|
||||
"description": "Track tweet performance and engagement metrics",
|
||||
"status": "coming_soon",
|
||||
"icon": "📊"
|
||||
},
|
||||
{
|
||||
"name": "A/B Testing Assistant",
|
||||
"description": "Test and optimize tweet variations for better results",
|
||||
"status": "coming_soon",
|
||||
"icon": "🔍"
|
||||
}
|
||||
]
|
||||
},
|
||||
"research": {
|
||||
"title": "Research & Intelligence",
|
||||
"icon": "🔎",
|
||||
"description": "Gain insights and stay ahead of trends",
|
||||
"features": [
|
||||
{
|
||||
"name": "Market Research",
|
||||
"description": "Analyze competitors and track industry trends",
|
||||
"status": "coming_soon",
|
||||
"icon": "📊"
|
||||
},
|
||||
{
|
||||
"name": "Content Inspiration",
|
||||
"description": "Get trending topic suggestions and content ideas",
|
||||
"status": "coming_soon",
|
||||
"icon": "💡"
|
||||
}
|
||||
]
|
||||
def apply_modern_styling():
|
||||
"""Apply modern CSS styling to the dashboard."""
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Import Google Fonts */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/* Global Styles */
|
||||
.stApp {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.main-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
margin: 1rem;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 2rem 0;
|
||||
background: linear-gradient(135deg, #1DA1F2, #0C85D0);
|
||||
border-radius: 16px;
|
||||
color: white;
|
||||
box-shadow: 0 10px 30px rgba(29, 161, 242, 0.3);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Feature Cards */
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #2D3748;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: #718096;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-status {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: linear-gradient(135deg, #48BB78, #38A169);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-coming-soon {
|
||||
background: linear-gradient(135deg, #ED8936, #DD6B20);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Metrics Cards */
|
||||
.metric-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||
border-left: 4px solid #1DA1F2;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #2D3748;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #718096;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.stButton > button {
|
||||
background: linear-gradient(135deg, #1DA1F2, #0C85D0);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(29, 161, 242, 0.3);
|
||||
}
|
||||
|
||||
.stButton > button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(29, 161, 242, 0.4);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.stTabs [data-baseweb="tab-list"] {
|
||||
gap: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
color: #4A5568;
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stTabs [aria-selected="true"] {
|
||||
background: white;
|
||||
color: #1DA1F2;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Connection Status */
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: linear-gradient(135deg, #C6F6D5, #9AE6B4);
|
||||
color: #22543D;
|
||||
border: 1px solid #9AE6B4;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: linear-gradient(135deg, #FED7D7, #FEB2B2);
|
||||
color: #742A2A;
|
||||
border: 1px solid #FEB2B2;
|
||||
}
|
||||
|
||||
/* Quick Actions */
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
background: white;
|
||||
border: 2px solid #E2E8F0;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
border-color: #1DA1F2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(29, 161, 242, 0.15);
|
||||
}
|
||||
|
||||
.quick-action-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.quick-action-title {
|
||||
font-weight: 600;
|
||||
color: #2D3748;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.quick-action-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
/* Analytics Charts */
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def run_dashboard():
|
||||
"""Main function to run the Twitter dashboard."""
|
||||
# Initialize dashboard
|
||||
dashboard = TwitterDashboard()
|
||||
def render_connection_status():
|
||||
"""Render Twitter connection status with modern styling."""
|
||||
# Simulate connection status (replace with real authentication check)
|
||||
is_connected = get_from_session("twitter_connected", False)
|
||||
|
||||
# Load feature data
|
||||
features = load_feature_data()
|
||||
|
||||
# Setup navigation
|
||||
sidebar = Sidebar(title="Twitter Tools")
|
||||
sidebar.add_menu_item("Dashboard", "📊", "dashboard")
|
||||
sidebar.add_menu_item("Tweet Generator", "✍️", "tweet_generator")
|
||||
sidebar.add_menu_item("Analytics", "📈", "analytics")
|
||||
sidebar.add_menu_item("Settings", "⚙️", "settings")
|
||||
|
||||
# Setup header
|
||||
header = Header(
|
||||
title="Twitter AI Writer",
|
||||
subtitle="Your all-in-one Twitter content creation and management platform"
|
||||
)
|
||||
header.add_action("New Tweet", "✏️", lambda: save_to_session("current_page", "tweet_generator"))
|
||||
header.add_action("Refresh", "🔄", lambda: st.experimental_rerun())
|
||||
|
||||
# Setup tabs
|
||||
tabs = Tabs()
|
||||
tabs.add_tab("Overview", "📊", lambda: render_overview(features))
|
||||
tabs.add_tab("Recent Tweets", "🐦", lambda: render_recent_tweets())
|
||||
tabs.add_tab("Analytics", "📈", lambda: render_analytics())
|
||||
|
||||
# Setup breadcrumbs
|
||||
breadcrumbs = Breadcrumbs()
|
||||
breadcrumbs.add_item("Home", "dashboard", "🏠")
|
||||
breadcrumbs.add_item(get_from_session("current_page", "Dashboard").title())
|
||||
|
||||
# Render dashboard
|
||||
dashboard.render()
|
||||
if is_connected:
|
||||
user_info = get_from_session("twitter_user", {"name": "Demo User", "handle": "@demo_user"})
|
||||
st.markdown(f"""
|
||||
<div class="connection-status status-connected">
|
||||
<span style="font-size: 1.2rem;">✅</span>
|
||||
<div>
|
||||
<strong>Connected as {user_info['name']}</strong>
|
||||
<div style="font-size: 0.9rem; opacity: 0.8;">{user_info['handle']}</div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
st.markdown("""
|
||||
<div class="connection-status status-disconnected">
|
||||
<span style="font-size: 1.2rem;">⚠️</span>
|
||||
<div>
|
||||
<strong>Twitter Not Connected</strong>
|
||||
<div style="font-size: 0.9rem; opacity: 0.8;">Connect your account to access all features</div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if st.button("🔗 Connect Twitter Account", key="connect_twitter"):
|
||||
# Simulate connection (replace with real OAuth flow)
|
||||
save_to_session("twitter_connected", True)
|
||||
save_to_session("twitter_user", {"name": "Demo User", "handle": "@demo_user"})
|
||||
st.rerun()
|
||||
|
||||
def render_overview(features: Dict):
|
||||
"""Render the overview tab content."""
|
||||
# Feature cards
|
||||
col1, col2, col3 = st.columns(3)
|
||||
def render_dashboard_header():
|
||||
"""Render the modern dashboard header."""
|
||||
st.markdown("""
|
||||
<div class="dashboard-header">
|
||||
<h1 class="dashboard-title">🐦 Twitter AI Dashboard</h1>
|
||||
<p class="dashboard-subtitle">Create, analyze, and optimize your Twitter content with AI-powered tools</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_quick_actions():
|
||||
"""Render quick action buttons."""
|
||||
st.markdown("### 🚀 Quick Actions")
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
FeatureCard(
|
||||
title="Tweet Generator",
|
||||
description="Create engaging tweets with AI assistance",
|
||||
icon="✍️",
|
||||
features=[
|
||||
{
|
||||
"name": "AI-Powered",
|
||||
"description": "Generate tweets using advanced AI"
|
||||
},
|
||||
{
|
||||
"name": "Customizable",
|
||||
"description": "Adjust tone, length, and style"
|
||||
}
|
||||
],
|
||||
on_click=lambda: save_to_session("current_page", "tweet_generator")
|
||||
).render()
|
||||
if st.button("✍️ Create Tweet", use_container_width=True, key="quick_tweet"):
|
||||
st.session_state.current_page = "tweet_generator"
|
||||
st.rerun()
|
||||
|
||||
with col2:
|
||||
FeatureCard(
|
||||
title="Analytics",
|
||||
description="Track your tweet performance",
|
||||
icon="📈",
|
||||
features=[
|
||||
{
|
||||
"name": "Engagement",
|
||||
"description": "Monitor likes, retweets, and replies"
|
||||
},
|
||||
{
|
||||
"name": "Growth",
|
||||
"description": "Track follower growth over time"
|
||||
}
|
||||
]
|
||||
).render()
|
||||
if st.button("📊 View Analytics", use_container_width=True, key="quick_analytics"):
|
||||
st.session_state.current_page = "analytics"
|
||||
st.rerun()
|
||||
|
||||
with col3:
|
||||
FeatureCard(
|
||||
title="Settings",
|
||||
description="Customize your experience",
|
||||
icon="⚙️",
|
||||
features=[
|
||||
{
|
||||
"name": "Preferences",
|
||||
"description": "Set your default options"
|
||||
},
|
||||
{
|
||||
"name": "API",
|
||||
"description": "Configure Twitter API settings"
|
||||
}
|
||||
]
|
||||
).render()
|
||||
if st.button("📅 Content Calendar", use_container_width=True, key="quick_calendar"):
|
||||
show_info_message("Content Calendar feature coming soon!")
|
||||
|
||||
with col4:
|
||||
if st.button("⚙️ Settings", use_container_width=True, key="quick_settings"):
|
||||
st.session_state.current_page = "settings"
|
||||
st.rerun()
|
||||
|
||||
def render_recent_tweets():
|
||||
"""Render the recent tweets tab content."""
|
||||
# Tweet form
|
||||
tweet_form = TweetForm(
|
||||
on_submit=lambda: handle_tweet_submit()
|
||||
def render_metrics_overview():
|
||||
"""Render key metrics overview."""
|
||||
st.markdown("### 📈 Performance Overview")
|
||||
|
||||
# Generate sample metrics (replace with real data)
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.markdown("""
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">1,234</div>
|
||||
<div class="metric-label">Total Tweets</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.markdown("""
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">45.2K</div>
|
||||
<div class="metric-label">Total Engagement</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
st.markdown("""
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">3.8%</div>
|
||||
<div class="metric-label">Engagement Rate</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col4:
|
||||
st.markdown("""
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">12.5K</div>
|
||||
<div class="metric-label">Followers</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_engagement_chart():
|
||||
"""Render engagement trends chart."""
|
||||
st.markdown("### 📊 Engagement Trends")
|
||||
|
||||
# Generate sample data (replace with real Twitter data)
|
||||
dates = pd.date_range(start=datetime.now() - timedelta(days=30), periods=30)
|
||||
engagement = np.random.normal(100, 20, 30)
|
||||
engagement = np.maximum(engagement, 0) # Ensure positive values
|
||||
|
||||
df = pd.DataFrame({
|
||||
'Date': dates,
|
||||
'Engagement': engagement,
|
||||
'Likes': engagement * 0.6,
|
||||
'Retweets': engagement * 0.3,
|
||||
'Replies': engagement * 0.1
|
||||
})
|
||||
|
||||
# Create interactive chart
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1,
|
||||
subplot_titles=('Total Engagement', 'Engagement Breakdown'),
|
||||
vertical_spacing=0.1,
|
||||
row_heights=[0.7, 0.3]
|
||||
)
|
||||
tweet_form.render()
|
||||
|
||||
# Recent tweets
|
||||
st.markdown("### Recent Tweets")
|
||||
tweets = get_from_session("tweets", [])
|
||||
for tweet in tweets:
|
||||
TweetCard(
|
||||
content=tweet["content"],
|
||||
engagement_score=tweet["engagement_score"],
|
||||
hashtags=tweet["hashtags"],
|
||||
emojis=tweet["emojis"],
|
||||
metrics=tweet["metrics"],
|
||||
on_copy=lambda: copy_tweet(tweet),
|
||||
on_save=lambda: save_tweet(tweet)
|
||||
).render()
|
||||
|
||||
def render_analytics():
|
||||
"""Render the analytics tab content."""
|
||||
st.markdown("### Tweet Analytics")
|
||||
st.info("Analytics features coming soon!")
|
||||
|
||||
def handle_tweet_submit():
|
||||
"""Handle tweet form submission."""
|
||||
# Get form data
|
||||
content = get_from_session("tweet_content")
|
||||
tone = get_from_session("tone")
|
||||
length = get_from_session("length")
|
||||
hashtags = get_from_session("hashtags")
|
||||
emojis = get_from_session("emojis")
|
||||
engagement_boost = get_from_session("engagement_boost")
|
||||
# Main engagement line
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['Date'],
|
||||
y=df['Engagement'],
|
||||
mode='lines+markers',
|
||||
name='Total Engagement',
|
||||
line=dict(color='#1DA1F2', width=3),
|
||||
marker=dict(size=6)
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Create tweet object
|
||||
tweet = {
|
||||
"content": content,
|
||||
"tone": tone,
|
||||
"length": length,
|
||||
"hashtags": hashtags,
|
||||
"emojis": emojis,
|
||||
"engagement_score": engagement_boost,
|
||||
"metrics": {
|
||||
"Engagement": engagement_boost,
|
||||
"Reach": engagement_boost * 0.8,
|
||||
"Growth": engagement_boost * 0.6
|
||||
# Stacked area chart for breakdown
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['Date'],
|
||||
y=df['Likes'],
|
||||
mode='lines',
|
||||
name='Likes',
|
||||
fill='tonexty',
|
||||
line=dict(color='#E53E3E')
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['Date'],
|
||||
y=df['Retweets'],
|
||||
mode='lines',
|
||||
name='Retweets',
|
||||
fill='tonexty',
|
||||
line=dict(color='#38A169')
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df['Date'],
|
||||
y=df['Replies'],
|
||||
mode='lines',
|
||||
name='Replies',
|
||||
fill='tonexty',
|
||||
line=dict(color='#D69E2E')
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
height=500,
|
||||
showlegend=True,
|
||||
hovermode='x unified',
|
||||
plot_bgcolor='rgba(0,0,0,0)',
|
||||
paper_bgcolor='rgba(0,0,0,0)'
|
||||
)
|
||||
|
||||
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(0,0,0,0.1)')
|
||||
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(0,0,0,0.1)')
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
def render_feature_grid():
|
||||
"""Render the feature grid with modern cards."""
|
||||
st.markdown("### 🛠️ Available Tools")
|
||||
|
||||
features = [
|
||||
{
|
||||
"title": "Smart Tweet Generator",
|
||||
"description": "Create engaging tweets with AI assistance, hashtag suggestions, and emoji optimization",
|
||||
"icon": "✨",
|
||||
"status": "active",
|
||||
"action": "tweet_generator"
|
||||
},
|
||||
{
|
||||
"title": "Performance Predictor",
|
||||
"description": "Predict tweet engagement and find optimal posting times",
|
||||
"icon": "🔮",
|
||||
"status": "coming_soon",
|
||||
"action": None
|
||||
},
|
||||
{
|
||||
"title": "Content Calendar",
|
||||
"description": "Plan and schedule your Twitter content strategy",
|
||||
"icon": "📅",
|
||||
"status": "coming_soon",
|
||||
"action": None
|
||||
},
|
||||
{
|
||||
"title": "Hashtag Research",
|
||||
"description": "Discover trending hashtags and analyze their performance",
|
||||
"icon": "#️⃣",
|
||||
"status": "coming_soon",
|
||||
"action": None
|
||||
},
|
||||
{
|
||||
"title": "Visual Content",
|
||||
"description": "Create quote cards, infographics, and visual tweets",
|
||||
"icon": "🎨",
|
||||
"status": "coming_soon",
|
||||
"action": None
|
||||
},
|
||||
{
|
||||
"title": "Analytics Dashboard",
|
||||
"description": "Deep dive into your Twitter performance metrics",
|
||||
"icon": "📊",
|
||||
"status": "coming_soon",
|
||||
"action": None
|
||||
}
|
||||
]
|
||||
|
||||
# Create grid layout
|
||||
cols = st.columns(3)
|
||||
|
||||
for i, feature in enumerate(features):
|
||||
with cols[i % 3]:
|
||||
status_class = "status-active" if feature["status"] == "active" else "status-coming-soon"
|
||||
|
||||
card_html = f"""
|
||||
<div class="feature-card" onclick="handleFeatureClick('{feature['action']}')">
|
||||
<span class="feature-icon">{feature['icon']}</span>
|
||||
<h3 class="feature-title">{feature['title']}</h3>
|
||||
<p class="feature-description">{feature['description']}</p>
|
||||
<span class="feature-status {status_class}">{feature['status'].replace('_', ' ')}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
st.markdown(card_html, unsafe_allow_html=True)
|
||||
|
||||
# Add button for active features
|
||||
if feature["status"] == "active" and feature["action"]:
|
||||
if st.button(f"Launch {feature['title']}", key=f"launch_{i}", use_container_width=True):
|
||||
st.session_state.current_page = feature["action"]
|
||||
st.rerun()
|
||||
|
||||
def render_recent_activity():
|
||||
"""Render recent activity feed."""
|
||||
st.markdown("### 📱 Recent Activity")
|
||||
|
||||
# Sample activity data (replace with real data)
|
||||
activities = [
|
||||
{"time": "2 hours ago", "action": "Generated tweet", "details": "AI-powered content about social media trends"},
|
||||
{"time": "5 hours ago", "action": "Analyzed performance", "details": "Tweet received 45 likes and 12 retweets"},
|
||||
{"time": "1 day ago", "action": "Scheduled tweet", "details": "Content scheduled for optimal posting time"},
|
||||
{"time": "2 days ago", "action": "Updated hashtags", "details": "Added trending hashtags to improve reach"}
|
||||
]
|
||||
|
||||
for activity in activities:
|
||||
st.markdown(f"""
|
||||
<div style="
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-left: 3px solid #1DA1F2;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
">
|
||||
<div style="font-weight: 600; color: #2D3748; margin-bottom: 0.25rem;">
|
||||
{activity['action']}
|
||||
</div>
|
||||
<div style="color: #718096; font-size: 0.9rem; margin-bottom: 0.25rem;">
|
||||
{activity['details']}
|
||||
</div>
|
||||
<div style="color: #A0AEC0; font-size: 0.8rem;">
|
||||
{activity['time']}
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def run_dashboard():
|
||||
"""Main function to run the enhanced Twitter dashboard."""
|
||||
# Apply modern styling
|
||||
apply_modern_styling()
|
||||
|
||||
# Initialize session state
|
||||
if "current_page" not in st.session_state:
|
||||
st.session_state.current_page = "dashboard"
|
||||
|
||||
# Handle page navigation
|
||||
if st.session_state.current_page == "tweet_generator":
|
||||
if st.button("← Back to Dashboard", key="back_to_dashboard"):
|
||||
st.session_state.current_page = "dashboard"
|
||||
st.rerun()
|
||||
smart_tweet_generator()
|
||||
return
|
||||
|
||||
# Main dashboard container
|
||||
st.markdown('<div class="main-container">', unsafe_allow_html=True)
|
||||
|
||||
# Render dashboard header
|
||||
render_dashboard_header()
|
||||
|
||||
# Render connection status
|
||||
render_connection_status()
|
||||
|
||||
# Create main layout
|
||||
tab1, tab2, tab3 = st.tabs(["🏠 Overview", "📊 Analytics", "⚙️ Settings"])
|
||||
|
||||
with tab1:
|
||||
# Quick actions
|
||||
render_quick_actions()
|
||||
|
||||
# Metrics overview
|
||||
render_metrics_overview()
|
||||
|
||||
# Feature grid
|
||||
render_feature_grid()
|
||||
|
||||
# Recent activity
|
||||
col1, col2 = st.columns([2, 1])
|
||||
with col1:
|
||||
render_engagement_chart()
|
||||
with col2:
|
||||
render_recent_activity()
|
||||
|
||||
with tab2:
|
||||
st.markdown("### 📈 Advanced Analytics")
|
||||
|
||||
# Time range selector
|
||||
col1, col2 = st.columns([1, 3])
|
||||
with col1:
|
||||
time_range = st.selectbox(
|
||||
"Time Range",
|
||||
["Last 7 days", "Last 30 days", "Last 90 days", "Last year"],
|
||||
index=1
|
||||
)
|
||||
|
||||
# Detailed analytics
|
||||
render_engagement_chart()
|
||||
|
||||
# Performance insights
|
||||
st.markdown("### 💡 Performance Insights")
|
||||
|
||||
insights = [
|
||||
"Your tweets perform 23% better when posted between 2-4 PM",
|
||||
"Tweets with 2-3 hashtags get 15% more engagement",
|
||||
"Visual content increases engagement by 35%",
|
||||
"Questions in tweets boost replies by 28%"
|
||||
]
|
||||
|
||||
for insight in insights:
|
||||
st.info(f"💡 {insight}")
|
||||
|
||||
with tab3:
|
||||
st.markdown("### ⚙️ Dashboard Settings")
|
||||
|
||||
# Twitter API settings
|
||||
with st.expander("🔑 Twitter API Configuration", expanded=False):
|
||||
st.markdown("Configure your Twitter API credentials to enable full functionality.")
|
||||
|
||||
api_key = st.text_input("API Key", type="password", help="Your Twitter API key")
|
||||
api_secret = st.text_input("API Secret", type="password", help="Your Twitter API secret")
|
||||
access_token = st.text_input("Access Token", type="password", help="Your Twitter access token")
|
||||
access_token_secret = st.text_input("Access Token Secret", type="password", help="Your Twitter access token secret")
|
||||
|
||||
if st.button("Save API Configuration"):
|
||||
# Save configuration (implement secure storage)
|
||||
show_success_message("API configuration saved successfully!")
|
||||
|
||||
# Dashboard preferences
|
||||
with st.expander("🎨 Dashboard Preferences", expanded=True):
|
||||
theme = st.selectbox("Theme", ["Light", "Dark", "Auto"], index=0)
|
||||
default_tone = st.selectbox("Default Tweet Tone", ["Professional", "Casual", "Humorous", "Inspirational"], index=1)
|
||||
auto_hashtags = st.checkbox("Auto-suggest hashtags", value=True)
|
||||
|
||||
if st.button("Save Preferences"):
|
||||
show_success_message("Preferences saved successfully!")
|
||||
|
||||
# Account management
|
||||
with st.expander("👤 Account Management", expanded=False):
|
||||
st.markdown("Manage your connected Twitter accounts and permissions.")
|
||||
|
||||
if get_from_session("twitter_connected", False):
|
||||
st.success("✅ Twitter account connected")
|
||||
if st.button("Disconnect Account"):
|
||||
save_to_session("twitter_connected", False)
|
||||
st.rerun()
|
||||
else:
|
||||
st.warning("⚠️ No Twitter account connected")
|
||||
if st.button("Connect Account"):
|
||||
save_to_session("twitter_connected", True)
|
||||
st.rerun()
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# JavaScript for handling feature clicks
|
||||
st.markdown("""
|
||||
<script>
|
||||
function handleFeatureClick(action) {
|
||||
if (action && action !== 'null') {
|
||||
// This would trigger a Streamlit rerun with the selected action
|
||||
console.log('Feature clicked:', action);
|
||||
}
|
||||
|
||||
# Add to tweets list
|
||||
tweets = get_from_session("tweets", [])
|
||||
tweets.append(tweet)
|
||||
save_to_session("tweets", tweets)
|
||||
|
||||
# Show success message
|
||||
show_success_message("Tweet created successfully!")
|
||||
|
||||
def copy_tweet(tweet: Dict):
|
||||
"""Copy tweet to clipboard."""
|
||||
show_success_message("Tweet copied to clipboard!")
|
||||
|
||||
def save_tweet(tweet: Dict):
|
||||
"""Save tweet for later."""
|
||||
show_success_message("Tweet saved!")
|
||||
}
|
||||
</script>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_dashboard()
|
||||
@@ -1,174 +1,634 @@
|
||||
"""
|
||||
Card components for Twitter UI.
|
||||
Provides consistent card layouts for features and tweets.
|
||||
Enhanced UI Cards with modern styling and improved functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, Optional, List
|
||||
from ..styles.theme import Theme
|
||||
from typing import Dict, List, Optional, Callable
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from datetime import datetime
|
||||
|
||||
class BaseCard:
|
||||
"""Base class for all card components."""
|
||||
def apply_cards_styling():
|
||||
"""Apply modern CSS styling for cards."""
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Modern Card Styles */
|
||||
.modern-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modern-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.modern-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(135deg, #1DA1F2, #0C85D0);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 0.75rem 0;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #E1E8ED;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(29, 161, 242, 0.15);
|
||||
border-color: #1DA1F2;
|
||||
}
|
||||
|
||||
.feature-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #E6F7FF, #F0F9FF);
|
||||
border-radius: 12px;
|
||||
border: 2px solid #91D5FF;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #2D3748;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.feature-description {
|
||||
color: #657786;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-stats {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #E1E8ED;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1DA1F2;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.8rem;
|
||||
color: #657786;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tweet-card {
|
||||
background: white;
|
||||
border: 1px solid #E1E8ED;
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tweet-card::before {
|
||||
content: "🐦";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 20px;
|
||||
background: white;
|
||||
padding: 0 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tweet-content {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
color: #14171A;
|
||||
margin-bottom: 1rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.tweet-metadata {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #657786;
|
||||
font-size: 0.9rem;
|
||||
border-top: 1px solid #E1E8ED;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.engagement-badge {
|
||||
background: linear-gradient(135deg, #52C41A, #73D13D);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.character-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.char-good { background: #E6F7FF; color: #1890FF; }
|
||||
.char-warning { background: #FFF7E6; color: #FA8C16; }
|
||||
.char-danger { background: #FFF1F0; color: #F5222D; }
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: #F7F9FA;
|
||||
border: 1px solid #E1E8ED;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
color: #657786;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: #1DA1F2;
|
||||
color: white;
|
||||
border-color: #1DA1F2;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: #1DA1F2;
|
||||
color: white;
|
||||
border-color: #1DA1F2;
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
background: #0C85D0;
|
||||
border-color: #0C85D0;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #E1E8ED;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1DA1F2;
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.8rem;
|
||||
color: #657786;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.modern-card, .feature-card, .tweet-card {
|
||||
margin: 0.5rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.feature-card-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-stats {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
class FeatureCard:
|
||||
"""Modern feature card component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
description: str,
|
||||
icon: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
actions: Optional[List[Dict[str, Any]]] = None
|
||||
icon: str = "🔧",
|
||||
stats: Optional[Dict[str, any]] = None,
|
||||
actions: Optional[List[Dict]] = None,
|
||||
on_click: Optional[Callable] = None
|
||||
):
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.icon = icon
|
||||
self.status = status
|
||||
self.stats = stats or {}
|
||||
self.actions = actions or []
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the card with consistent styling."""
|
||||
with st.container():
|
||||
st.markdown(f"""
|
||||
<div class="card">
|
||||
<div style="display: flex; align-items: center; margin-bottom: {Theme.SPACING["sm"]};">
|
||||
{f'<span style="font-size: 1.5em; margin-right: {Theme.SPACING["sm"]};">{self.icon}</span>' if self.icon else ''}
|
||||
<h3 style="margin: 0;">{self.title}</h3>
|
||||
</div>
|
||||
<p style="color: {Theme.COLORS["text_secondary"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||
{self.description}
|
||||
</p>
|
||||
{f'<span class="status-badge status-{self.status}">{self.status.title()}</span>' if self.status else ''}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if self.actions:
|
||||
cols = st.columns(len(self.actions))
|
||||
for i, action in enumerate(self.actions):
|
||||
with cols[i]:
|
||||
if st.button(
|
||||
action["label"],
|
||||
key=f"action_{i}",
|
||||
help=action.get("help"),
|
||||
use_container_width=True
|
||||
):
|
||||
action["callback"]()
|
||||
|
||||
class FeatureCard(BaseCard):
|
||||
"""Card component for displaying features."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
description: str,
|
||||
icon: str,
|
||||
status: str = "active",
|
||||
features: Optional[List[Dict[str, Any]]] = None,
|
||||
on_click: Optional[callable] = None
|
||||
):
|
||||
super().__init__(title, description, icon, status)
|
||||
self.features = features or []
|
||||
self.on_click = on_click
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the feature card with enhanced styling."""
|
||||
with st.container():
|
||||
st.markdown(f"""
|
||||
<div class="card feature-card">
|
||||
<div style="display: flex; align-items: center; margin-bottom: {Theme.SPACING["sm"]};">
|
||||
<span style="font-size: 1.5em; margin-right: {Theme.SPACING["sm"]};">{self.icon}</span>
|
||||
<h3 style="margin: 0;">{self.title}</h3>
|
||||
def render(self):
|
||||
"""Render the feature card."""
|
||||
apply_cards_styling()
|
||||
|
||||
# Create stats HTML
|
||||
stats_html = ""
|
||||
if self.stats:
|
||||
stats_items = []
|
||||
for label, value in self.stats.items():
|
||||
stats_items.append(f"""
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{value}</span>
|
||||
<span class="stat-label">{label}</span>
|
||||
</div>
|
||||
<p style="color: {Theme.COLORS["text_secondary"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||
{self.description}
|
||||
</p>
|
||||
<span class="status-badge status-{self.status}">{self.status.title()}</span>
|
||||
""")
|
||||
stats_html = f"""
|
||||
<div class="feature-stats">
|
||||
{''.join(stats_items)}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if self.features:
|
||||
for feature in self.features:
|
||||
st.markdown(f"""
|
||||
<div style="margin-left: {Theme.SPACING["lg"]}; margin-top: {Theme.SPACING["sm"]};">
|
||||
<p style="margin: 0;">
|
||||
<strong>{feature["name"]}</strong>: {feature["description"]}
|
||||
</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if self.on_click:
|
||||
if st.button(
|
||||
f"Launch {self.title}",
|
||||
key=f"launch_{self.title.lower().replace(' ', '_')}",
|
||||
use_container_width=True
|
||||
):
|
||||
self.on_click()
|
||||
"""
|
||||
|
||||
# Create actions HTML
|
||||
actions_html = ""
|
||||
if self.actions:
|
||||
action_buttons = []
|
||||
for action in self.actions:
|
||||
button_class = "action-button"
|
||||
if action.get("primary", False):
|
||||
button_class += " primary"
|
||||
|
||||
action_buttons.append(f"""
|
||||
<button class="{button_class}" onclick="{action.get('onclick', '')}">
|
||||
{action.get('icon', '')} {action.get('label', 'Action')}
|
||||
</button>
|
||||
""")
|
||||
actions_html = f"""
|
||||
<div class="card-actions">
|
||||
{''.join(action_buttons)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Render the card
|
||||
card_html = f"""
|
||||
<div class="feature-card" onclick="{self.on_click or ''}">
|
||||
<div class="feature-card-header">
|
||||
<div class="feature-icon">{self.icon}</div>
|
||||
<div>
|
||||
<h3 class="feature-title">{self.title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p class="feature-description">{self.description}</p>
|
||||
{stats_html}
|
||||
{actions_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
st.markdown(card_html, unsafe_allow_html=True)
|
||||
|
||||
class TweetCard(BaseCard):
|
||||
"""Card component for displaying tweets."""
|
||||
class TweetCard:
|
||||
"""Modern tweet card component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: str,
|
||||
engagement_score: float,
|
||||
hashtags: List[str],
|
||||
emojis: List[str],
|
||||
metrics: Optional[Dict[str, Any]] = None,
|
||||
on_copy: Optional[callable] = None,
|
||||
on_save: Optional[callable] = None
|
||||
engagement_score: int = 0,
|
||||
hashtags: List[str] = None,
|
||||
emojis: List[str] = None,
|
||||
metrics: Optional[Dict] = None,
|
||||
timestamp: Optional[str] = None,
|
||||
on_copy: Optional[Callable] = None,
|
||||
on_save: Optional[Callable] = None,
|
||||
on_edit: Optional[Callable] = None,
|
||||
on_post: Optional[Callable] = None
|
||||
):
|
||||
super().__init__(
|
||||
title="Tweet",
|
||||
description=content,
|
||||
icon="🐦",
|
||||
actions=[
|
||||
{
|
||||
"label": "Copy",
|
||||
"callback": on_copy or (lambda: None),
|
||||
"help": "Copy tweet to clipboard"
|
||||
},
|
||||
{
|
||||
"label": "Save",
|
||||
"callback": on_save or (lambda: None),
|
||||
"help": "Save tweet for later"
|
||||
}
|
||||
]
|
||||
)
|
||||
self.content = content
|
||||
self.engagement_score = engagement_score
|
||||
self.hashtags = hashtags
|
||||
self.emojis = emojis
|
||||
self.hashtags = hashtags or []
|
||||
self.emojis = emojis or []
|
||||
self.metrics = metrics or {}
|
||||
self.timestamp = timestamp or datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
self.on_copy = on_copy
|
||||
self.on_save = on_save
|
||||
self.on_edit = on_edit
|
||||
self.on_post = on_post
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the tweet card with metrics and actions."""
|
||||
with st.container():
|
||||
st.markdown(f"""
|
||||
<div class="card tweet-card">
|
||||
<div style="display: flex; align-items: center; margin-bottom: {Theme.SPACING["sm"]};">
|
||||
<span style="font-size: 1.5em; margin-right: {Theme.SPACING["sm"]};">{self.icon}</span>
|
||||
<h3 style="margin: 0;">Tweet</h3>
|
||||
</div>
|
||||
<p style="color: {Theme.COLORS["text"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||
{self.description}
|
||||
</p>
|
||||
<div style="display: flex; gap: {Theme.SPACING["sm"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||
{''.join(f'<span style="color: {Theme.COLORS["primary"]};">{tag}</span>' for tag in self.hashtags)}
|
||||
</div>
|
||||
<div style="display: flex; gap: {Theme.SPACING["sm"]}; margin: {Theme.SPACING["sm"]} 0;">
|
||||
{''.join(f'<span>{emoji}</span>' for emoji in self.emojis)}
|
||||
</div>
|
||||
<div style="margin-top: {Theme.SPACING["md"]};">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>Engagement Score: {self.engagement_score}%</span>
|
||||
<div style="display: flex; gap: {Theme.SPACING["sm"]};">
|
||||
<button class="stButton" onclick="copyTweet()">Copy</button>
|
||||
<button class="stButton" onclick="saveTweet()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
def _get_character_info(self):
|
||||
"""Get character count information."""
|
||||
full_text = f"{self.content} {' '.join(self.hashtags)}"
|
||||
count = len(full_text)
|
||||
remaining = 280 - count
|
||||
|
||||
if count <= 240:
|
||||
status_class = "char-good"
|
||||
elif count <= 270:
|
||||
status_class = "char-warning"
|
||||
else:
|
||||
status_class = "char-danger"
|
||||
|
||||
return {
|
||||
"count": count,
|
||||
"remaining": remaining,
|
||||
"status_class": status_class
|
||||
}
|
||||
|
||||
def render(self):
|
||||
"""Render the tweet card."""
|
||||
apply_cards_styling()
|
||||
|
||||
char_info = self._get_character_info()
|
||||
full_content = f"{self.content} {' '.join(self.hashtags)}"
|
||||
|
||||
# Create metrics HTML
|
||||
metrics_html = ""
|
||||
if self.metrics:
|
||||
metric_items = []
|
||||
for label, value in self.metrics.items():
|
||||
metric_items.append(f"""
|
||||
<div class="metric-card">
|
||||
<span class="metric-value">{value}</span>
|
||||
<span class="metric-label">{label}</span>
|
||||
</div>
|
||||
""")
|
||||
metrics_html = f"""
|
||||
<div class="metrics-grid">
|
||||
{''.join(metric_items)}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if self.metrics:
|
||||
cols = st.columns(len(self.metrics))
|
||||
for i, (metric, value) in enumerate(self.metrics.items()):
|
||||
with cols[i]:
|
||||
st.metric(metric, f"{value}%")
|
||||
"""
|
||||
|
||||
# Create actions
|
||||
actions = []
|
||||
if self.on_copy:
|
||||
actions.append('<button class="action-button" onclick="copyTweet()">📋 Copy</button>')
|
||||
if self.on_save:
|
||||
actions.append('<button class="action-button" onclick="saveTweet()">💾 Save</button>')
|
||||
if self.on_edit:
|
||||
actions.append('<button class="action-button" onclick="editTweet()">✏️ Edit</button>')
|
||||
if self.on_post:
|
||||
actions.append('<button class="action-button primary" onclick="postTweet()">🐦 Post</button>')
|
||||
|
||||
actions_html = f'<div class="card-actions">{"".join(actions)}</div>' if actions else ""
|
||||
|
||||
# Render the card
|
||||
card_html = f"""
|
||||
<div class="tweet-card">
|
||||
<div class="tweet-content">{full_content}</div>
|
||||
{metrics_html}
|
||||
<div class="tweet-metadata">
|
||||
<div class="engagement-badge">
|
||||
📊 {self.engagement_score}% Engagement
|
||||
</div>
|
||||
<div class="character-badge {char_info['status_class']}">
|
||||
{char_info['count']}/280
|
||||
</div>
|
||||
</div>
|
||||
{actions_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
st.markdown(card_html, unsafe_allow_html=True)
|
||||
|
||||
class MetricsCard:
|
||||
"""Modern metrics display card."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
metrics: Dict[str, any],
|
||||
chart_data: Optional[Dict] = None,
|
||||
trend: Optional[str] = None
|
||||
):
|
||||
self.title = title
|
||||
self.metrics = metrics
|
||||
self.chart_data = chart_data
|
||||
self.trend = trend
|
||||
|
||||
def render(self):
|
||||
"""Render the metrics card."""
|
||||
apply_cards_styling()
|
||||
|
||||
# Create metrics grid
|
||||
metric_items = []
|
||||
for label, value in self.metrics.items():
|
||||
metric_items.append(f"""
|
||||
<div class="metric-card">
|
||||
<span class="metric-value">{value}</span>
|
||||
<span class="metric-label">{label}</span>
|
||||
</div>
|
||||
""")
|
||||
|
||||
metrics_grid = f"""
|
||||
<div class="metrics-grid">
|
||||
{''.join(metric_items)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Add trend indicator
|
||||
trend_html = ""
|
||||
if self.trend:
|
||||
trend_color = "#52C41A" if "up" in self.trend.lower() else "#F5222D"
|
||||
trend_icon = "📈" if "up" in self.trend.lower() else "📉"
|
||||
trend_html = f"""
|
||||
<div style="text-align: center; margin-top: 1rem; color: {trend_color};">
|
||||
{trend_icon} {self.trend}
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Render the card
|
||||
card_html = f"""
|
||||
<div class="modern-card">
|
||||
<h3 style="margin-bottom: 1rem; color: #2D3748;">{self.title}</h3>
|
||||
{metrics_grid}
|
||||
{trend_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
st.markdown(card_html, unsafe_allow_html=True)
|
||||
|
||||
# Add chart if provided
|
||||
if self.chart_data:
|
||||
self._render_chart()
|
||||
|
||||
def _render_chart(self):
|
||||
"""Render chart for metrics."""
|
||||
if self.chart_data.get("type") == "line":
|
||||
fig = px.line(
|
||||
x=self.chart_data.get("x", []),
|
||||
y=self.chart_data.get("y", []),
|
||||
title=self.chart_data.get("title", ""),
|
||||
labels=self.chart_data.get("labels", {})
|
||||
)
|
||||
elif self.chart_data.get("type") == "bar":
|
||||
fig = px.bar(
|
||||
x=self.chart_data.get("x", []),
|
||||
y=self.chart_data.get("y", []),
|
||||
title=self.chart_data.get("title", ""),
|
||||
labels=self.chart_data.get("labels", {})
|
||||
)
|
||||
else:
|
||||
return
|
||||
|
||||
fig.update_layout(
|
||||
plot_bgcolor='rgba(0,0,0,0)',
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
showlegend=False,
|
||||
height=300
|
||||
)
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
class StatusCard:
|
||||
"""Status indicator card."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
status: str,
|
||||
message: str,
|
||||
icon: str = "ℹ️",
|
||||
actions: Optional[List[Dict]] = None
|
||||
):
|
||||
self.title = title
|
||||
self.status = status # success, warning, error, info
|
||||
self.message = message
|
||||
self.icon = icon
|
||||
self.actions = actions or []
|
||||
|
||||
def render(self):
|
||||
"""Render the status card."""
|
||||
apply_cards_styling()
|
||||
|
||||
# Status colors
|
||||
status_colors = {
|
||||
"success": "#52C41A",
|
||||
"warning": "#FA8C16",
|
||||
"error": "#F5222D",
|
||||
"info": "#1890FF"
|
||||
}
|
||||
|
||||
color = status_colors.get(self.status, "#1890FF")
|
||||
|
||||
# Create actions
|
||||
actions_html = ""
|
||||
if self.actions:
|
||||
action_buttons = []
|
||||
for action in self.actions:
|
||||
action_buttons.append(f"""
|
||||
<button class="action-button" onclick="{action.get('onclick', '')}">
|
||||
{action.get('icon', '')} {action.get('label', 'Action')}
|
||||
</button>
|
||||
""")
|
||||
actions_html = f"""
|
||||
<div class="card-actions">
|
||||
{''.join(action_buttons)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Render the card
|
||||
card_html = f"""
|
||||
<div class="modern-card" style="border-left: 4px solid {color};">
|
||||
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||
<span style="font-size: 2rem;">{self.icon}</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; color: #2D3748;">{self.title}</h3>
|
||||
<span style="color: {color}; font-weight: 600; text-transform: uppercase; font-size: 0.8rem;">
|
||||
{self.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color: #657786; margin-bottom: 1rem;">{self.message}</p>
|
||||
{actions_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
st.markdown(card_html, unsafe_allow_html=True)
|
||||
|
||||
# Utility functions for creating common cards
|
||||
def create_feature_card(title: str, description: str, icon: str = "🔧", **kwargs):
|
||||
"""Create and render a feature card."""
|
||||
card = FeatureCard(title, description, icon, **kwargs)
|
||||
card.render()
|
||||
|
||||
def create_tweet_card(content: str, **kwargs):
|
||||
"""Create and render a tweet card."""
|
||||
card = TweetCard(content, **kwargs)
|
||||
card.render()
|
||||
|
||||
def create_metrics_card(title: str, metrics: Dict, **kwargs):
|
||||
"""Create and render a metrics card."""
|
||||
card = MetricsCard(title, metrics, **kwargs)
|
||||
card.render()
|
||||
|
||||
def create_status_card(title: str, status: str, message: str, **kwargs):
|
||||
"""Create and render a status card."""
|
||||
card = StatusCard(title, status, message, **kwargs)
|
||||
card.render()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,232 +1,554 @@
|
||||
"""
|
||||
Navigation components for Twitter UI.
|
||||
Provides consistent navigation and layout structure.
|
||||
Enhanced Navigation Component for Twitter UI with modern styling and improved functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, Optional, List
|
||||
from typing import Dict, List, Optional, Callable, Any
|
||||
from ..styles.theme import Theme
|
||||
import os
|
||||
|
||||
def apply_navigation_styling():
|
||||
"""Apply modern CSS styling for navigation components."""
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Navigation Styles */
|
||||
.nav-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #E2E8F0;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #1DA1F2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: linear-gradient(135deg, #52C41A, #73D13D);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: linear-gradient(135deg, #FA8C16, #FFA940);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
background: #F7F9FA;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
color: #657786;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #E1F5FE;
|
||||
border-color: #1DA1F2;
|
||||
color: #1DA1F2;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(29, 161, 242, 0.2);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: linear-gradient(135deg, #1DA1F2, #0C85D0);
|
||||
color: white;
|
||||
border-color: #1DA1F2;
|
||||
box-shadow: 0 4px 15px rgba(29, 161, 242, 0.3);
|
||||
}
|
||||
|
||||
.nav-item.active:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(29, 161, 242, 0.4);
|
||||
}
|
||||
|
||||
.nav-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #657786;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: #CBD5E0;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: linear-gradient(135deg, #52C41A, #73D13D);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: #F7F9FA;
|
||||
color: #657786;
|
||||
border: 1px solid #E1E8ED;
|
||||
}
|
||||
|
||||
.action-button.secondary:hover {
|
||||
background: #E1F5FE;
|
||||
color: #1DA1F2;
|
||||
border-color: #1DA1F2;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.nav-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
class TwitterNavigation:
|
||||
"""Enhanced navigation component for Twitter dashboard."""
|
||||
|
||||
def __init__(self, theme: Optional[Theme] = None):
|
||||
self.theme = theme or Theme()
|
||||
self.current_page = st.session_state.get('current_page', 'dashboard')
|
||||
|
||||
def render_header(self, title: str = "Twitter AI Assistant", show_status: bool = True):
|
||||
"""Render the navigation header with title and status."""
|
||||
apply_navigation_styling()
|
||||
|
||||
st.markdown('<div class="nav-container">', unsafe_allow_html=True)
|
||||
st.markdown('<div class="nav-header">', unsafe_allow_html=True)
|
||||
|
||||
# Title
|
||||
st.markdown(f'<div class="nav-title">🐦 {title}</div>', unsafe_allow_html=True)
|
||||
|
||||
# Status indicator
|
||||
if show_status:
|
||||
twitter_connected = self._check_twitter_connection()
|
||||
status_class = "status-connected" if twitter_connected else "status-disconnected"
|
||||
status_text = "Connected" if twitter_connected else "Not Connected"
|
||||
status_icon = "✅" if twitter_connected else "⚠️"
|
||||
|
||||
st.markdown(f'''
|
||||
<div class="nav-status {status_class}">
|
||||
{status_icon} Twitter {status_text}
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def render_menu(self, menu_items: List[Dict], current_page: Optional[str] = None):
|
||||
"""Render navigation menu with items."""
|
||||
if current_page:
|
||||
self.current_page = current_page
|
||||
st.session_state.current_page = current_page
|
||||
|
||||
st.markdown('<div class="nav-menu">', unsafe_allow_html=True)
|
||||
|
||||
cols = st.columns(len(menu_items))
|
||||
|
||||
for i, item in enumerate(menu_items):
|
||||
with cols[i]:
|
||||
active_class = "active" if item.get('key') == self.current_page else ""
|
||||
|
||||
if st.button(
|
||||
f"{item.get('icon', '')} {item.get('label', '')}",
|
||||
key=f"nav_{item.get('key', i)}",
|
||||
use_container_width=True,
|
||||
type="primary" if active_class else "secondary"
|
||||
):
|
||||
st.session_state.current_page = item.get('key')
|
||||
if item.get('callback'):
|
||||
item['callback']()
|
||||
st.rerun()
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
return st.session_state.get('current_page', menu_items[0].get('key'))
|
||||
|
||||
def render_breadcrumb(self, items: List[Dict]):
|
||||
"""Render breadcrumb navigation."""
|
||||
st.markdown('<div class="nav-breadcrumb">', unsafe_allow_html=True)
|
||||
|
||||
for i, item in enumerate(items):
|
||||
if i > 0:
|
||||
st.markdown('<span class="breadcrumb-separator">›</span>', unsafe_allow_html=True)
|
||||
|
||||
icon = item.get('icon', '')
|
||||
label = item.get('label', '')
|
||||
|
||||
if item.get('active', False):
|
||||
st.markdown(f'<span class="breadcrumb-item"><strong>{icon} {label}</strong></span>', unsafe_allow_html=True)
|
||||
else:
|
||||
st.markdown(f'<span class="breadcrumb-item">{icon} {label}</span>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def render_actions(self, actions: List[Dict]):
|
||||
"""Render action buttons in navigation."""
|
||||
st.markdown('<div class="nav-actions">', unsafe_allow_html=True)
|
||||
|
||||
cols = st.columns(len(actions))
|
||||
|
||||
for i, action in enumerate(actions):
|
||||
with cols[i]:
|
||||
button_type = action.get('type', 'primary')
|
||||
|
||||
if st.button(
|
||||
f"{action.get('icon', '')} {action.get('label', '')}",
|
||||
key=f"action_{action.get('key', i)}",
|
||||
type=button_type,
|
||||
use_container_width=True,
|
||||
help=action.get('help', '')
|
||||
):
|
||||
if action.get('callback'):
|
||||
action['callback']()
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def render_sidebar_menu(self, menu_items: List[Dict]):
|
||||
"""Render sidebar navigation menu."""
|
||||
with st.sidebar:
|
||||
st.markdown("### 🐦 Twitter Tools")
|
||||
|
||||
for item in menu_items:
|
||||
icon = item.get('icon', '')
|
||||
label = item.get('label', '')
|
||||
key = item.get('key', '')
|
||||
|
||||
if st.button(f"{icon} {label}", key=f"sidebar_{key}", use_container_width=True):
|
||||
st.session_state.current_page = key
|
||||
if item.get('callback'):
|
||||
item['callback']()
|
||||
st.rerun()
|
||||
|
||||
# Twitter connection status in sidebar
|
||||
st.markdown("---")
|
||||
twitter_connected = self._check_twitter_connection()
|
||||
|
||||
if twitter_connected:
|
||||
st.success("🐦 Twitter Connected")
|
||||
else:
|
||||
st.warning("⚠️ Twitter Not Connected")
|
||||
if st.button("🔧 Configure Twitter", use_container_width=True):
|
||||
st.session_state.show_twitter_config = True
|
||||
st.rerun()
|
||||
|
||||
def _check_twitter_connection(self) -> bool:
|
||||
"""Check if Twitter is connected."""
|
||||
twitter_config = st.session_state.get('twitter_config', {})
|
||||
return bool(twitter_config and all([
|
||||
twitter_config.get('api_key'),
|
||||
twitter_config.get('api_secret'),
|
||||
twitter_config.get('access_token'),
|
||||
twitter_config.get('access_token_secret')
|
||||
]))
|
||||
|
||||
class Sidebar:
|
||||
"""Sidebar navigation component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str = "Twitter Tools",
|
||||
logo: Optional[str] = None,
|
||||
menu_items: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
def __init__(self, title: str = "Navigation", logo: Optional[str] = None):
|
||||
"""Initialize the sidebar."""
|
||||
self.title = title
|
||||
self.logo = logo
|
||||
self.menu_items = menu_items or []
|
||||
self.menu_items = []
|
||||
|
||||
def add_menu_item(
|
||||
self,
|
||||
label: str,
|
||||
icon: str,
|
||||
page: str,
|
||||
badge: Optional[str] = None
|
||||
) -> None:
|
||||
def add_menu_item(self, label: str, icon: str, key: str, callback: Optional[Callable] = None):
|
||||
"""Add a menu item to the sidebar."""
|
||||
self.menu_items.append({
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"page": page,
|
||||
"badge": badge
|
||||
'label': label,
|
||||
'icon': icon,
|
||||
'key': key,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the sidebar with consistent styling."""
|
||||
def render(self) -> str:
|
||||
"""Render the sidebar and return the selected page."""
|
||||
with st.sidebar:
|
||||
# Logo and title
|
||||
if self.logo:
|
||||
try:
|
||||
import os
|
||||
if os.path.exists(self.logo):
|
||||
st.image(self.logo, width=50)
|
||||
else:
|
||||
# Show a placeholder or just skip the logo
|
||||
st.markdown("🐦", help="Twitter Tools Logo")
|
||||
except Exception as e:
|
||||
# If there's any error loading the image, show an emoji instead
|
||||
st.markdown("🐦", help="Twitter Tools Logo")
|
||||
|
||||
st.markdown(f"""
|
||||
<h2 style="margin: {Theme.SPACING["sm"]} 0;">{self.title}</h2>
|
||||
""", unsafe_allow_html=True)
|
||||
if self.logo and os.path.exists(self.logo):
|
||||
st.image(self.logo, width=100)
|
||||
st.title(self.title)
|
||||
st.markdown("---")
|
||||
|
||||
# Menu items
|
||||
selected_page = None
|
||||
for item in self.menu_items:
|
||||
st.markdown(f"""
|
||||
<div class="menu-item">
|
||||
<span style="font-size: 1.2em; margin-right: {Theme.SPACING["sm"]};">{item["icon"]}</span>
|
||||
<span>{item["label"]}</span>
|
||||
{f'<span class="badge">{item["badge"]}</span>' if item.get("badge") else ""}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if st.button(
|
||||
item["label"],
|
||||
key=f"nav_{item['page']}",
|
||||
f"{item['icon']} {item['label']}",
|
||||
key=f"sidebar_{item['key']}",
|
||||
use_container_width=True
|
||||
):
|
||||
st.session_state["current_page"] = item["page"]
|
||||
selected_page = item['key']
|
||||
if item.get('callback'):
|
||||
item['callback']()
|
||||
|
||||
return selected_page or st.session_state.get('current_page', 'dashboard')
|
||||
|
||||
|
||||
class Header:
|
||||
"""Header navigation component."""
|
||||
"""Header component with title and actions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
subtitle: Optional[str] = None,
|
||||
actions: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
def __init__(self, title: str = "Dashboard", subtitle: str = ""):
|
||||
"""Initialize the header."""
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.actions = actions or []
|
||||
self.actions = []
|
||||
|
||||
def add_action(
|
||||
self,
|
||||
label: str,
|
||||
icon: str,
|
||||
callback: callable,
|
||||
help_text: Optional[str] = None
|
||||
) -> None:
|
||||
def add_action(self, label: str, icon: str, callback: Callable, help_text: str = ""):
|
||||
"""Add an action button to the header."""
|
||||
self.actions.append({
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"callback": callback,
|
||||
"help_text": help_text
|
||||
'label': label,
|
||||
'icon': icon,
|
||||
'callback': callback,
|
||||
'help': help_text
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the header with consistent styling."""
|
||||
# Build action buttons HTML
|
||||
action_buttons = []
|
||||
for action in self.actions:
|
||||
help_text = action.get("help_text", "")
|
||||
action_buttons.append(f"""
|
||||
<button class="header-action" title="{help_text}">
|
||||
<span style="font-size: 1.2em; margin-right: {Theme.SPACING["xs"]};">{action["icon"]}</span>
|
||||
{action["label"]}
|
||||
</button>
|
||||
""")
|
||||
def render(self):
|
||||
"""Render the header."""
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
st.markdown(f"""
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1 style="margin: 0;">{self.title}</h1>
|
||||
{f'<p style="color: {Theme.COLORS["text_secondary"]}; margin: {Theme.SPACING["xs"]} 0;">{self.subtitle}</p>' if self.subtitle else ""}
|
||||
</div>
|
||||
<div style="display: flex; gap: {Theme.SPACING["sm"]};">
|
||||
{''.join(action_buttons)}
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
with col1:
|
||||
st.title(f"{self.title}")
|
||||
if self.subtitle:
|
||||
st.markdown(f"*{self.subtitle}*")
|
||||
|
||||
# Add action button callbacks
|
||||
for i, action in enumerate(self.actions):
|
||||
if st.button(
|
||||
action["label"],
|
||||
key=f"header_action_{i}",
|
||||
help=action.get("help_text")
|
||||
):
|
||||
action["callback"]()
|
||||
with col2:
|
||||
if self.actions:
|
||||
for i, action in enumerate(self.actions):
|
||||
if st.button(
|
||||
f"{action['icon']} {action['label']}",
|
||||
key=f"header_action_{i}",
|
||||
help=action.get('help', ''),
|
||||
use_container_width=True
|
||||
):
|
||||
action['callback']()
|
||||
|
||||
|
||||
class Tabs:
|
||||
"""Tab navigation component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tabs: Optional[List[Dict[str, Any]]] = None,
|
||||
default_tab: Optional[str] = None
|
||||
):
|
||||
self.tabs = tabs or []
|
||||
self.default_tab = default_tab
|
||||
def __init__(self):
|
||||
"""Initialize the tabs."""
|
||||
self.tabs = []
|
||||
|
||||
def add_tab(
|
||||
self,
|
||||
label: str,
|
||||
icon: Optional[str] = None,
|
||||
content: Optional[callable] = None
|
||||
) -> None:
|
||||
"""Add a tab to the navigation."""
|
||||
def add_tab(self, label: str, icon: str, content_func: Callable):
|
||||
"""Add a tab."""
|
||||
self.tabs.append({
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"content": content
|
||||
'label': label,
|
||||
'icon': icon,
|
||||
'content_func': content_func
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the tabs with consistent styling."""
|
||||
def render(self):
|
||||
"""Render the tabs."""
|
||||
if not self.tabs:
|
||||
return
|
||||
|
||||
# Create tab labels with icons
|
||||
tab_labels = [
|
||||
f"{tab['icon']} {tab['label']}" if tab.get('icon') else tab['label']
|
||||
for tab in self.tabs
|
||||
]
|
||||
tab_labels = [f"{tab['icon']} {tab['label']}" for tab in self.tabs]
|
||||
selected_tabs = st.tabs(tab_labels)
|
||||
|
||||
# Get current tab from session state or use default
|
||||
current_tab = st.session_state.get("current_tab", self.default_tab or self.tabs[0]["label"])
|
||||
|
||||
# Render tabs
|
||||
selected_tab = st.tabs(tab_labels)[tab_labels.index(current_tab)]
|
||||
|
||||
# Update session state
|
||||
st.session_state["current_tab"] = current_tab
|
||||
|
||||
# Render tab content
|
||||
with selected_tab:
|
||||
for tab in self.tabs:
|
||||
if tab["label"] == current_tab and tab.get("content"):
|
||||
tab["content"]()
|
||||
for i, tab in enumerate(self.tabs):
|
||||
with selected_tabs[i]:
|
||||
tab['content_func']()
|
||||
|
||||
|
||||
class Breadcrumbs:
|
||||
"""Breadcrumb navigation component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
items: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
self.items = items or []
|
||||
def __init__(self):
|
||||
"""Initialize breadcrumbs."""
|
||||
self.items = []
|
||||
|
||||
def add_item(
|
||||
self,
|
||||
label: str,
|
||||
page: Optional[str] = None,
|
||||
icon: Optional[str] = None
|
||||
) -> None:
|
||||
def add_item(self, label: str, key: str = None, callback: Callable = None):
|
||||
"""Add a breadcrumb item."""
|
||||
self.items.append({
|
||||
"label": label,
|
||||
"page": page,
|
||||
"icon": icon
|
||||
'label': label,
|
||||
'key': key,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the breadcrumbs with consistent styling."""
|
||||
def render(self):
|
||||
"""Render the breadcrumbs."""
|
||||
if not self.items:
|
||||
return
|
||||
|
||||
breadcrumb_items = []
|
||||
for i, item in enumerate(self.items):
|
||||
icon_html = f'<span style="font-size: 1.2em; margin-right: {Theme.SPACING["xs"]};">{item["icon"]}</span>' if item.get("icon") else ""
|
||||
link_html = f'<a href="#" onclick="setPage(\'{item["page"]}\')">{item["label"]}</a>' if item.get("page") else f'<span>{item["label"]}</span>'
|
||||
separator = f'<span style="margin: 0 {Theme.SPACING["xs"]};">/</span>' if i < len(self.items) - 1 else ""
|
||||
|
||||
breadcrumb_items.append(f"""
|
||||
<span class="breadcrumb-item">
|
||||
{icon_html}
|
||||
{link_html}
|
||||
</span>
|
||||
{separator}
|
||||
""")
|
||||
breadcrumb_html = '<div class="nav-breadcrumb">'
|
||||
|
||||
st.markdown(f"""
|
||||
<div class="breadcrumbs">
|
||||
{''.join(breadcrumb_items)}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
for i, item in enumerate(self.items):
|
||||
if i > 0:
|
||||
breadcrumb_html += '<span class="breadcrumb-separator">›</span>'
|
||||
|
||||
if item.get('callback'):
|
||||
breadcrumb_html += f'<span class="breadcrumb-item clickable" onclick="handleBreadcrumbClick(\'{item["key"]}\')">{item["label"]}</span>'
|
||||
else:
|
||||
breadcrumb_html += f'<span class="breadcrumb-item">{item["label"]}</span>'
|
||||
|
||||
breadcrumb_html += '</div>'
|
||||
st.markdown(breadcrumb_html, unsafe_allow_html=True)
|
||||
|
||||
|
||||
def create_main_navigation() -> TwitterNavigation:
|
||||
"""Create and return the main navigation instance."""
|
||||
return TwitterNavigation()
|
||||
|
||||
def render_page_header(title: str, subtitle: str = "", icon: str = ""):
|
||||
"""Render a consistent page header."""
|
||||
st.markdown(f"""
|
||||
<div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, #E6F7FF, #F0F9FF); border-radius: 16px;">
|
||||
<h1 style="color: #1DA1F2; margin-bottom: 0.5rem;">{icon} {title}</h1>
|
||||
{f'<p style="color: #657786; font-size: 1.1rem;">{subtitle}</p>' if subtitle else ''}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_quick_actions(actions: List[Dict]):
|
||||
"""Render quick action buttons."""
|
||||
st.markdown("### ⚡ Quick Actions")
|
||||
|
||||
cols = st.columns(len(actions))
|
||||
|
||||
for i, action in enumerate(actions):
|
||||
with cols[i]:
|
||||
if st.button(
|
||||
f"{action.get('icon', '')} {action.get('label', '')}",
|
||||
key=f"quick_action_{i}",
|
||||
use_container_width=True,
|
||||
help=action.get('help', '')
|
||||
):
|
||||
if action.get('callback'):
|
||||
action['callback']()
|
||||
|
||||
# Default menu items for Twitter dashboard
|
||||
DEFAULT_MENU_ITEMS = [
|
||||
{
|
||||
'key': 'dashboard',
|
||||
'label': 'Dashboard',
|
||||
'icon': '🏠',
|
||||
'help': 'Main dashboard overview'
|
||||
},
|
||||
{
|
||||
'key': 'generator',
|
||||
'label': 'Tweet Generator',
|
||||
'icon': '✨',
|
||||
'help': 'AI-powered tweet generation'
|
||||
},
|
||||
{
|
||||
'key': 'analytics',
|
||||
'label': 'Analytics',
|
||||
'icon': '📊',
|
||||
'help': 'Tweet performance analytics'
|
||||
},
|
||||
{
|
||||
'key': 'scheduler',
|
||||
'label': 'Scheduler',
|
||||
'icon': '📅',
|
||||
'help': 'Schedule tweets for later'
|
||||
},
|
||||
{
|
||||
'key': 'settings',
|
||||
'label': 'Settings',
|
||||
'icon': '⚙️',
|
||||
'help': 'Twitter account and API settings'
|
||||
}
|
||||
]
|
||||
|
||||
DEFAULT_QUICK_ACTIONS = [
|
||||
{
|
||||
'key': 'new_tweet',
|
||||
'label': 'New Tweet',
|
||||
'icon': '✍️',
|
||||
'help': 'Create a new tweet'
|
||||
},
|
||||
{
|
||||
'key': 'ai_generate',
|
||||
'label': 'AI Generate',
|
||||
'icon': '🤖',
|
||||
'help': 'Generate tweets with AI'
|
||||
},
|
||||
{
|
||||
'key': 'view_analytics',
|
||||
'label': 'View Analytics',
|
||||
'icon': '📈',
|
||||
'help': 'Check tweet performance'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,503 @@
|
||||
"""
|
||||
Enhanced Twitter Dashboard with real authentication and posting capabilities.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
# Import our enhanced components
|
||||
from .components.navigation import TwitterNavigation, create_main_navigation
|
||||
from .components.cards import TwitterCard, create_analytics_card, create_tweet_card
|
||||
from .components.forms import TweetForm, TwitterConfigForm
|
||||
from ..tweet_generator.smart_tweet_generator import (
|
||||
smart_tweet_generator,
|
||||
post_tweet_to_twitter,
|
||||
get_real_tweet_analytics,
|
||||
render_twitter_authentication
|
||||
)
|
||||
from ....integrations.twitter_auth_bridge import (
|
||||
TwitterAuthBridge,
|
||||
save_twitter_credentials,
|
||||
load_twitter_credentials,
|
||||
is_twitter_authenticated,
|
||||
setup_twitter_session,
|
||||
clear_twitter_session
|
||||
)
|
||||
|
||||
# Initialize authentication bridge
|
||||
auth_bridge = TwitterAuthBridge()
|
||||
|
||||
def initialize_dashboard():
|
||||
"""Initialize the Twitter dashboard with proper styling and state management."""
|
||||
|
||||
# Apply custom CSS
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main-dashboard {
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 25px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Initialize session state
|
||||
if 'twitter_dashboard_initialized' not in st.session_state:
|
||||
st.session_state.twitter_dashboard_initialized = True
|
||||
st.session_state.current_page = 'dashboard'
|
||||
st.session_state.tweet_drafts = []
|
||||
st.session_state.posted_tweets = []
|
||||
st.session_state.analytics_data = {}
|
||||
|
||||
def render_dashboard_header():
|
||||
"""Render the main dashboard header with connection status."""
|
||||
|
||||
st.markdown('<div class="dashboard-header">', unsafe_allow_html=True)
|
||||
|
||||
col1, col2, col3 = st.columns([1, 2, 1])
|
||||
|
||||
with col2:
|
||||
st.markdown('<h1 class="dashboard-title">🐦 Twitter AI Dashboard</h1>', unsafe_allow_html=True)
|
||||
st.markdown('<p class="dashboard-subtitle">AI-Powered Tweet Generation & Analytics</p>', unsafe_allow_html=True)
|
||||
|
||||
# Connection status
|
||||
is_connected = is_twitter_authenticated()
|
||||
|
||||
if is_connected:
|
||||
user_info = st.session_state.get('twitter_user', {})
|
||||
username = user_info.get('screen_name', 'Unknown')
|
||||
st.markdown(f'''
|
||||
<div class="status-indicator status-connected">
|
||||
✅ Connected as @{username}
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
else:
|
||||
st.markdown('''
|
||||
<div class="status-indicator status-disconnected">
|
||||
❌ Not Connected to Twitter
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def render_quick_actions():
|
||||
"""Render quick action buttons."""
|
||||
|
||||
st.markdown("### 🚀 Quick Actions")
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
if st.button("📝 Generate Tweet", key="quick_generate", help="Create AI-powered tweets"):
|
||||
st.session_state.current_page = 'generate'
|
||||
st.rerun()
|
||||
|
||||
with col2:
|
||||
if st.button("📊 View Analytics", key="quick_analytics", help="View tweet performance"):
|
||||
st.session_state.current_page = 'analytics'
|
||||
st.rerun()
|
||||
|
||||
with col3:
|
||||
if st.button("⚙️ Settings", key="quick_settings", help="Configure Twitter connection"):
|
||||
st.session_state.current_page = 'settings'
|
||||
st.rerun()
|
||||
|
||||
with col4:
|
||||
if st.button("📋 Drafts", key="quick_drafts", help="Manage tweet drafts"):
|
||||
st.session_state.current_page = 'drafts'
|
||||
st.rerun()
|
||||
|
||||
def render_dashboard_overview():
|
||||
"""Render the main dashboard overview with metrics."""
|
||||
|
||||
if not is_twitter_authenticated():
|
||||
st.warning("⚠️ Please connect your Twitter account to view dashboard metrics.")
|
||||
if st.button("Connect Twitter Account", type="primary"):
|
||||
st.session_state.current_page = 'settings'
|
||||
st.rerun()
|
||||
return
|
||||
|
||||
# Get user metrics
|
||||
user_info = st.session_state.get('twitter_user', {})
|
||||
|
||||
# Display metrics
|
||||
st.markdown("### 📈 Account Overview")
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.markdown(f'''
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{user_info.get('followers_count', 0):,}</div>
|
||||
<div class="metric-label">Followers</div>
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.markdown(f'''
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{user_info.get('friends_count', 0):,}</div>
|
||||
<div class="metric-label">Following</div>
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
posted_count = len(st.session_state.get('posted_tweets', []))
|
||||
st.markdown(f'''
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{posted_count}</div>
|
||||
<div class="metric-label">Posted Today</div>
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
with col4:
|
||||
draft_count = len(st.session_state.get('tweet_drafts', []))
|
||||
st.markdown(f'''
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{draft_count}</div>
|
||||
<div class="metric-label">Drafts</div>
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
# Recent activity
|
||||
st.markdown("### 📝 Recent Activity")
|
||||
|
||||
recent_tweets = st.session_state.get('posted_tweets', [])[-5:] # Last 5 tweets
|
||||
|
||||
if recent_tweets:
|
||||
for tweet in reversed(recent_tweets):
|
||||
with st.expander(f"Tweet: {tweet.get('text', '')[:50]}..."):
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.write(f"**Text:** {tweet.get('text', '')}")
|
||||
st.write(f"**Posted:** {tweet.get('created_at', '')}")
|
||||
|
||||
if tweet.get('metrics'):
|
||||
metrics = tweet['metrics']
|
||||
st.write(f"**Engagement:** {metrics.get('favorite_count', 0)} likes, "
|
||||
f"{metrics.get('retweet_count', 0)} retweets")
|
||||
|
||||
with col2:
|
||||
if st.button(f"View Analytics", key=f"analytics_{tweet.get('id')}"):
|
||||
st.session_state.selected_tweet_id = tweet.get('id')
|
||||
st.session_state.current_page = 'analytics'
|
||||
st.rerun()
|
||||
else:
|
||||
st.info("No recent tweets found. Start by generating and posting some content!")
|
||||
|
||||
def render_settings_page():
|
||||
"""Render the settings page for Twitter configuration."""
|
||||
|
||||
st.markdown("### ⚙️ Twitter Configuration")
|
||||
|
||||
# Twitter Authentication Section
|
||||
with st.expander("🔐 Twitter API Configuration", expanded=not is_twitter_authenticated()):
|
||||
render_twitter_authentication()
|
||||
|
||||
# Account Information
|
||||
if is_twitter_authenticated():
|
||||
st.markdown("### 👤 Account Information")
|
||||
|
||||
user_info = st.session_state.get('twitter_user', {})
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.write(f"**Username:** @{user_info.get('screen_name', 'N/A')}")
|
||||
st.write(f"**Display Name:** {user_info.get('name', 'N/A')}")
|
||||
st.write(f"**Followers:** {user_info.get('followers_count', 0):,}")
|
||||
|
||||
with col2:
|
||||
st.write(f"**Following:** {user_info.get('friends_count', 0):,}")
|
||||
st.write(f"**Tweets:** {user_info.get('statuses_count', 0):,}")
|
||||
st.write(f"**Account Created:** {user_info.get('created_at', 'N/A')}")
|
||||
|
||||
# Disconnect option
|
||||
st.markdown("---")
|
||||
if st.button("🔓 Disconnect Twitter Account", type="secondary"):
|
||||
clear_twitter_session()
|
||||
st.success("Twitter account disconnected successfully!")
|
||||
st.rerun()
|
||||
|
||||
def render_analytics_page():
|
||||
"""Render the analytics page with real Twitter metrics."""
|
||||
|
||||
st.markdown("### 📊 Tweet Analytics")
|
||||
|
||||
if not is_twitter_authenticated():
|
||||
st.warning("Please connect your Twitter account to view analytics.")
|
||||
return
|
||||
|
||||
# Tweet selection
|
||||
posted_tweets = st.session_state.get('posted_tweets', [])
|
||||
|
||||
if not posted_tweets:
|
||||
st.info("No tweets found. Generate and post some tweets to see analytics!")
|
||||
return
|
||||
|
||||
# Select tweet for analysis
|
||||
tweet_options = {
|
||||
f"{tweet.get('text', '')[:50]}... ({tweet.get('created_at', '')})": tweet.get('id')
|
||||
for tweet in posted_tweets
|
||||
}
|
||||
|
||||
selected_tweet_text = st.selectbox(
|
||||
"Select a tweet to analyze:",
|
||||
options=list(tweet_options.keys())
|
||||
)
|
||||
|
||||
if selected_tweet_text:
|
||||
tweet_id = tweet_options[selected_tweet_text]
|
||||
|
||||
# Get analytics
|
||||
with st.spinner("Loading analytics..."):
|
||||
analytics_result = asyncio.run(get_real_tweet_analytics(tweet_id))
|
||||
|
||||
if analytics_result.get('success'):
|
||||
analytics_data = analytics_result['data']
|
||||
|
||||
# Display metrics
|
||||
st.markdown("#### 📈 Performance Metrics")
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
metrics = analytics_data.get('metrics', {})
|
||||
|
||||
with col1:
|
||||
st.metric("Likes", metrics.get('likes', 0))
|
||||
|
||||
with col2:
|
||||
st.metric("Retweets", metrics.get('retweets', 0))
|
||||
|
||||
with col3:
|
||||
st.metric("Replies", metrics.get('replies', 0))
|
||||
|
||||
with col4:
|
||||
engagement = analytics_data.get('engagement', {})
|
||||
st.metric("Engagement Rate", f"{engagement.get('engagement_rate', 0):.2f}%")
|
||||
|
||||
# Detailed analytics
|
||||
st.markdown("#### 🔍 Detailed Analysis")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Engagement Breakdown:**")
|
||||
total_engagement = metrics.get('total_engagement', 0)
|
||||
st.write(f"• Total Engagement: {total_engagement}")
|
||||
st.write(f"• Likes Rate: {engagement.get('likes_rate', 0):.2f}%")
|
||||
st.write(f"• Retweets Rate: {engagement.get('retweets_rate', 0):.2f}%")
|
||||
|
||||
with col2:
|
||||
st.markdown("**Content Analysis:**")
|
||||
content_analysis = analytics_data.get('content_analysis', {})
|
||||
st.write(f"• Character Count: {content_analysis.get('character_count', 0)}")
|
||||
st.write(f"• Hashtags: {content_analysis.get('hashtag_count', 0)}")
|
||||
st.write(f"• Mentions: {content_analysis.get('mention_count', 0)}")
|
||||
|
||||
# Timing analysis
|
||||
timing = analytics_data.get('timing', {})
|
||||
if timing:
|
||||
st.markdown("#### ⏰ Timing Analysis")
|
||||
st.write(f"• Posted: {timing.get('posted_at', 'N/A')}")
|
||||
st.write(f"• Age: {timing.get('age_hours', 0):.1f} hours")
|
||||
st.write(f"• Peak Period: {timing.get('peak_engagement_period', 'N/A')}")
|
||||
st.write(f"• Engagement Velocity: {timing.get('engagement_velocity', 0):.2f} per hour")
|
||||
|
||||
else:
|
||||
st.error(f"Failed to load analytics: {analytics_result.get('error', 'Unknown error')}")
|
||||
|
||||
def render_drafts_page():
|
||||
"""Render the drafts management page."""
|
||||
|
||||
st.markdown("### 📋 Tweet Drafts")
|
||||
|
||||
drafts = st.session_state.get('tweet_drafts', [])
|
||||
|
||||
if not drafts:
|
||||
st.info("No drafts found. Create some tweets in the generator to save as drafts!")
|
||||
return
|
||||
|
||||
for i, draft in enumerate(drafts):
|
||||
with st.expander(f"Draft {i+1}: {draft.get('text', '')[:50]}..."):
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
st.write(f"**Text:** {draft.get('text', '')}")
|
||||
st.write(f"**Created:** {draft.get('created_at', '')}")
|
||||
if draft.get('hashtags'):
|
||||
st.write(f"**Hashtags:** {', '.join(draft['hashtags'])}")
|
||||
|
||||
with col2:
|
||||
if st.button(f"Post Now", key=f"post_draft_{i}"):
|
||||
if is_twitter_authenticated():
|
||||
with st.spinner("Posting tweet..."):
|
||||
result = asyncio.run(post_tweet_to_twitter(draft))
|
||||
|
||||
if result.get('success'):
|
||||
st.success("Tweet posted successfully!")
|
||||
# Move from drafts to posted
|
||||
st.session_state.posted_tweets.append(result['data'])
|
||||
st.session_state.tweet_drafts.pop(i)
|
||||
st.rerun()
|
||||
else:
|
||||
st.error(f"Failed to post: {result.get('error')}")
|
||||
else:
|
||||
st.error("Please connect your Twitter account first!")
|
||||
|
||||
if st.button(f"Delete", key=f"delete_draft_{i}"):
|
||||
st.session_state.tweet_drafts.pop(i)
|
||||
st.rerun()
|
||||
|
||||
def main_twitter_dashboard():
|
||||
"""Main Twitter dashboard function."""
|
||||
|
||||
# Initialize dashboard
|
||||
initialize_dashboard()
|
||||
|
||||
# Create navigation
|
||||
nav = TwitterNavigation()
|
||||
current_page = nav.render_main_navigation()
|
||||
|
||||
# Update session state if page changed
|
||||
if current_page != st.session_state.get('current_page'):
|
||||
st.session_state.current_page = current_page
|
||||
|
||||
# Render dashboard header
|
||||
render_dashboard_header()
|
||||
|
||||
# Route to appropriate page
|
||||
page = st.session_state.get('current_page', 'dashboard')
|
||||
|
||||
if page == 'dashboard':
|
||||
render_quick_actions()
|
||||
render_dashboard_overview()
|
||||
|
||||
elif page == 'generate':
|
||||
st.markdown("### 🤖 AI Tweet Generator")
|
||||
smart_tweet_generator()
|
||||
|
||||
elif page == 'analytics':
|
||||
render_analytics_page()
|
||||
|
||||
elif page == 'settings':
|
||||
render_settings_page()
|
||||
|
||||
elif page == 'drafts':
|
||||
render_drafts_page()
|
||||
|
||||
else:
|
||||
# Default to dashboard
|
||||
render_quick_actions()
|
||||
render_dashboard_overview()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_twitter_dashboard()
|
||||
Reference in New Issue
Block a user