Content Calendar, Content Gap Analysis, and Content Optimization
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
Card components for Twitter UI.
|
||||
Provides consistent card layouts for features and tweets.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, Optional, List
|
||||
from ..styles.theme import Theme
|
||||
|
||||
class BaseCard:
|
||||
"""Base class for all card components."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
description: str,
|
||||
icon: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
actions: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.icon = icon
|
||||
self.status = status
|
||||
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>
|
||||
</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>
|
||||
</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()
|
||||
|
||||
class TweetCard(BaseCard):
|
||||
"""Card component for displaying tweets."""
|
||||
|
||||
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
|
||||
):
|
||||
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.engagement_score = engagement_score
|
||||
self.hashtags = hashtags
|
||||
self.emojis = emojis
|
||||
self.metrics = metrics or {}
|
||||
|
||||
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>
|
||||
</div>
|
||||
</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}%")
|
||||
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Form components for Twitter UI.
|
||||
Provides consistent form layouts and input validation.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, Optional, List, Callable
|
||||
from ..styles.theme import Theme
|
||||
|
||||
class BaseForm:
|
||||
"""Base class for all form components."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
description: Optional[str] = None,
|
||||
on_submit: Optional[Callable] = None
|
||||
):
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.on_submit = on_submit
|
||||
self.fields: Dict[str, Any] = {}
|
||||
|
||||
def add_field(
|
||||
self,
|
||||
key: str,
|
||||
label: str,
|
||||
field_type: str = "text",
|
||||
required: bool = False,
|
||||
help_text: Optional[str] = None,
|
||||
options: Optional[List[str]] = None,
|
||||
default: Any = None,
|
||||
validation: Optional[Callable] = None
|
||||
) -> None:
|
||||
"""Add a field to the form."""
|
||||
self.fields[key] = {
|
||||
"label": label,
|
||||
"type": field_type,
|
||||
"required": required,
|
||||
"help_text": help_text,
|
||||
"options": options,
|
||||
"default": default,
|
||||
"validation": validation
|
||||
}
|
||||
|
||||
def validate(self) -> bool:
|
||||
"""Validate all form fields."""
|
||||
for key, field in self.fields.items():
|
||||
if field["required"] and not st.session_state.get(key):
|
||||
st.error(f"{field['label']} is required")
|
||||
return False
|
||||
if field["validation"] and not field["validation"](st.session_state.get(key)):
|
||||
return False
|
||||
return True
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the form with consistent styling."""
|
||||
with st.container():
|
||||
st.markdown(f"""
|
||||
<div class="form-container">
|
||||
<h3 style="margin-bottom: {Theme.SPACING['sm']};">{self.title}</h3>
|
||||
{f'<p style="color: {Theme.COLORS["text_secondary"]}; margin-bottom: {Theme.SPACING["md"]};">{self.description}</p>' if self.description else ''}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
for key, field in self.fields.items():
|
||||
if field["type"] == "text":
|
||||
st.text_input(
|
||||
field["label"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
value=field["default"]
|
||||
)
|
||||
elif field["type"] == "textarea":
|
||||
st.text_area(
|
||||
field["label"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
value=field["default"]
|
||||
)
|
||||
elif field["type"] == "select":
|
||||
st.selectbox(
|
||||
field["label"],
|
||||
options=field["options"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
index=field["options"].index(field["default"]) if field["default"] in field["options"] else 0
|
||||
)
|
||||
elif field["type"] == "multiselect":
|
||||
st.multiselect(
|
||||
field["label"],
|
||||
options=field["options"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
default=field["default"]
|
||||
)
|
||||
elif field["type"] == "number":
|
||||
st.number_input(
|
||||
field["label"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
value=field["default"]
|
||||
)
|
||||
elif field["type"] == "slider":
|
||||
st.slider(
|
||||
field["label"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
value=field["default"]
|
||||
)
|
||||
elif field["type"] == "checkbox":
|
||||
st.checkbox(
|
||||
field["label"],
|
||||
key=key,
|
||||
help=field["help_text"],
|
||||
value=field["default"]
|
||||
)
|
||||
|
||||
if st.button("Submit", use_container_width=True):
|
||||
if self.validate() and self.on_submit:
|
||||
self.on_submit()
|
||||
|
||||
class TweetForm(BaseForm):
|
||||
"""Form component for tweet generation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_submit: Optional[Callable] = None,
|
||||
default_tone: str = "professional",
|
||||
default_length: str = "medium"
|
||||
):
|
||||
super().__init__(
|
||||
title="Generate Tweet",
|
||||
description="Create engaging tweets with AI assistance",
|
||||
on_submit=on_submit
|
||||
)
|
||||
|
||||
# Add tweet content field
|
||||
self.add_field(
|
||||
"tweet_content",
|
||||
"Tweet Content",
|
||||
field_type="textarea",
|
||||
required=True,
|
||||
help_text="Enter your tweet content or topic"
|
||||
)
|
||||
|
||||
# Add tone selection
|
||||
self.add_field(
|
||||
"tone",
|
||||
"Tweet Tone",
|
||||
field_type="select",
|
||||
options=["professional", "casual", "humorous", "informative", "inspirational"],
|
||||
default=default_tone,
|
||||
help_text="Select the tone for your tweet"
|
||||
)
|
||||
|
||||
# Add length selection
|
||||
self.add_field(
|
||||
"length",
|
||||
"Tweet Length",
|
||||
field_type="select",
|
||||
options=["short", "medium", "long"],
|
||||
default=default_length,
|
||||
help_text="Select the desired length of your tweet"
|
||||
)
|
||||
|
||||
# Add hashtag options
|
||||
self.add_field(
|
||||
"hashtags",
|
||||
"Hashtags",
|
||||
field_type="multiselect",
|
||||
options=["#AI", "#Tech", "#Innovation", "#Business", "#Marketing"],
|
||||
help_text="Select relevant hashtags"
|
||||
)
|
||||
|
||||
# Add emoji options
|
||||
self.add_field(
|
||||
"emojis",
|
||||
"Emojis",
|
||||
field_type="multiselect",
|
||||
options=["🚀", "💡", "🎯", "🔥", "✨"],
|
||||
help_text="Select emojis to include"
|
||||
)
|
||||
|
||||
# Add engagement settings
|
||||
self.add_field(
|
||||
"engagement_boost",
|
||||
"Engagement Boost",
|
||||
field_type="slider",
|
||||
default=50,
|
||||
help_text="Adjust the engagement optimization level"
|
||||
)
|
||||
|
||||
class SettingsForm(BaseForm):
|
||||
"""Form component for user settings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
on_submit: Optional[Callable] = None,
|
||||
default_settings: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
super().__init__(
|
||||
title="User Settings",
|
||||
description="Customize your Twitter experience",
|
||||
on_submit=on_submit
|
||||
)
|
||||
|
||||
settings = default_settings or {}
|
||||
|
||||
# Add API settings
|
||||
self.add_field(
|
||||
"api_key",
|
||||
"Twitter API Key",
|
||||
field_type="text",
|
||||
help_text="Enter your Twitter API key",
|
||||
default=settings.get("api_key", "")
|
||||
)
|
||||
|
||||
# Add theme settings
|
||||
self.add_field(
|
||||
"theme",
|
||||
"Theme",
|
||||
field_type="select",
|
||||
options=["light", "dark", "system"],
|
||||
default=settings.get("theme", "system"),
|
||||
help_text="Select your preferred theme"
|
||||
)
|
||||
|
||||
# Add notification settings
|
||||
self.add_field(
|
||||
"notifications",
|
||||
"Enable Notifications",
|
||||
field_type="checkbox",
|
||||
default=settings.get("notifications", True),
|
||||
help_text="Receive notifications for important updates"
|
||||
)
|
||||
|
||||
# Add auto-save settings
|
||||
self.add_field(
|
||||
"auto_save",
|
||||
"Auto-save Drafts",
|
||||
field_type="checkbox",
|
||||
default=settings.get("auto_save", True),
|
||||
help_text="Automatically save tweet drafts"
|
||||
)
|
||||
|
||||
# Add language settings
|
||||
self.add_field(
|
||||
"language",
|
||||
"Language",
|
||||
field_type="select",
|
||||
options=["English", "Spanish", "French", "German", "Japanese"],
|
||||
default=settings.get("language", "English"),
|
||||
help_text="Select your preferred language"
|
||||
)
|
||||
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Navigation components for Twitter UI.
|
||||
Provides consistent navigation and layout structure.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, Optional, List
|
||||
from ..styles.theme import Theme
|
||||
|
||||
class Sidebar:
|
||||
"""Sidebar navigation component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str = "Twitter Tools",
|
||||
logo: Optional[str] = None,
|
||||
menu_items: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
self.title = title
|
||||
self.logo = logo
|
||||
self.menu_items = menu_items or []
|
||||
|
||||
def add_menu_item(
|
||||
self,
|
||||
label: str,
|
||||
icon: str,
|
||||
page: str,
|
||||
badge: Optional[str] = None
|
||||
) -> None:
|
||||
"""Add a menu item to the sidebar."""
|
||||
self.menu_items.append({
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"page": page,
|
||||
"badge": badge
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the sidebar with consistent styling."""
|
||||
with st.sidebar:
|
||||
# Logo and title
|
||||
if self.logo:
|
||||
st.image(self.logo, width=50)
|
||||
st.markdown(f"""
|
||||
<h2 style="margin: {Theme.SPACING["sm"]} 0;">{self.title}</h2>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Menu items
|
||||
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']}",
|
||||
use_container_width=True
|
||||
):
|
||||
st.session_state["current_page"] = item["page"]
|
||||
|
||||
class Header:
|
||||
"""Header navigation component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
subtitle: Optional[str] = None,
|
||||
actions: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.actions = actions or []
|
||||
|
||||
def add_action(
|
||||
self,
|
||||
label: str,
|
||||
icon: str,
|
||||
callback: callable,
|
||||
help_text: Optional[str] = None
|
||||
) -> None:
|
||||
"""Add an action button to the header."""
|
||||
self.actions.append({
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"callback": callback,
|
||||
"help_text": 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>
|
||||
""")
|
||||
|
||||
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)
|
||||
|
||||
# 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"]()
|
||||
|
||||
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 add_tab(
|
||||
self,
|
||||
label: str,
|
||||
icon: Optional[str] = None,
|
||||
content: Optional[callable] = None
|
||||
) -> None:
|
||||
"""Add a tab to the navigation."""
|
||||
self.tabs.append({
|
||||
"label": label,
|
||||
"icon": icon,
|
||||
"content": content
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the tabs with consistent styling."""
|
||||
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
|
||||
]
|
||||
|
||||
# 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"]()
|
||||
|
||||
class Breadcrumbs:
|
||||
"""Breadcrumb navigation component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
items: Optional[List[Dict[str, Any]]] = None
|
||||
):
|
||||
self.items = items or []
|
||||
|
||||
def add_item(
|
||||
self,
|
||||
label: str,
|
||||
page: Optional[str] = None,
|
||||
icon: Optional[str] = None
|
||||
) -> None:
|
||||
"""Add a breadcrumb item."""
|
||||
self.items.append({
|
||||
"label": label,
|
||||
"page": page,
|
||||
"icon": icon
|
||||
})
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the breadcrumbs with consistent styling."""
|
||||
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}
|
||||
""")
|
||||
|
||||
st.markdown(f"""
|
||||
<div class="breadcrumbs">
|
||||
{''.join(breadcrumb_items)}
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
Reference in New Issue
Block a user