fix: add metadata-based Stripe customer lookup in verify-checkout for reliable post-subscription plan detection (#538)
This commit is contained in:
@@ -1,102 +0,0 @@
|
||||
######################################################
|
||||
#
|
||||
# Alwrity, as an AI news writer, will have to be factually correct.
|
||||
# We will do multiple rounds of web research and cite our sources.
|
||||
# 'include_urls' will focus news articles only from well known sources.
|
||||
# Choosing a country will help us get better results.
|
||||
#
|
||||
######################################################
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from textwrap import dedent
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path('../../.env'))
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from ..ai_web_researcher.google_serp_search import perform_serper_news_search
|
||||
|
||||
|
||||
def ai_news_generation(news_keywords, news_country, news_language):
|
||||
""" Generate news aritcle based on given keywords. """
|
||||
# Use to store the blog in a string, to save in a *.md file.
|
||||
blog_markdown_str = ""
|
||||
|
||||
logger.info(f"Researching and Writing News Article on keywords: {news_keywords}")
|
||||
# Call on the got-researcher, tavily apis for this. Do google search for organic competition.
|
||||
try:
|
||||
google_news_result = perform_serper_news_search(news_keywords, news_country, news_language)
|
||||
blog_markdown_str = write_news_google_search(news_keywords, news_country, news_language, google_news_result)
|
||||
#print(blog_markdown_str)
|
||||
except Exception as err:
|
||||
logger.error(f"Failed in Google News web research: {err}")
|
||||
logger.info("\n######### Draft1: Finished News article from Google web search: ###########\n\n")
|
||||
return blog_markdown_str
|
||||
|
||||
|
||||
def write_news_google_search(news_keywords, news_country, news_language, search_results):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
news_language = get_language_name(news_language)
|
||||
news_country = get_country_name(news_country)
|
||||
|
||||
prompt = f"""
|
||||
As an experienced {news_language} news journalist and editor,
|
||||
I will provide you with my 'News keywords' and its 'google search results'.
|
||||
Your goal is to write a News report, backed by given google search results.
|
||||
Important, as a news report, its imperative that your content is factually correct and cited.
|
||||
|
||||
Follow below guidelines:
|
||||
1). Understand and utilize the provided google search result json.
|
||||
2). Always provide in-line citations and provide referance links.
|
||||
3). Understand the given news item and adapt your tone accordingly.
|
||||
4). Always include the dates when then news was reported.
|
||||
6). Do not explain, describe your response.
|
||||
7). Your blog should be highly formatted in markdown style and highly readable.
|
||||
8). Important: Please read the entire prompt before writing anything. Follow the prompt exactly as I instructed.
|
||||
|
||||
\n\nNews Keywords: "{news_keywords}"\n\n
|
||||
Google search Result: "{search_results}"
|
||||
"""
|
||||
logger.info("Generating blog and FAQs from Google web search results.")
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Exit: Failed to get response from LLM: {err}")
|
||||
exit(1)
|
||||
|
||||
|
||||
def get_language_name(language_code):
|
||||
languages = {
|
||||
"es": "Spanish",
|
||||
"vn": "Vietnamese",
|
||||
"en": "English",
|
||||
"ar": "Arabic",
|
||||
"hi": "Hindi",
|
||||
"de": "German",
|
||||
"zh-cn": "Chinese (Simplified)"
|
||||
# Add more language codes and corresponding names as needed
|
||||
}
|
||||
return languages.get(language_code, "Unknown")
|
||||
|
||||
def get_country_name(country_code):
|
||||
countries = {
|
||||
"es": "Spain",
|
||||
"vn": "Vietnam",
|
||||
"pk": "Pakistan",
|
||||
"in": "India",
|
||||
"de": "Germany",
|
||||
"cn": "China"
|
||||
# Add more country codes and corresponding names as needed
|
||||
}
|
||||
return countries.get(country_code, "Unknown")
|
||||
@@ -1,115 +0,0 @@
|
||||
import streamlit as st
|
||||
import json
|
||||
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def generate_product_description(title, details, audience, tone, length, keywords):
|
||||
"""
|
||||
Generates a product description using OpenAI's API.
|
||||
|
||||
Args:
|
||||
title (str): The title of the product.
|
||||
details (list): A list of product details (features, benefits, etc.).
|
||||
audience (list): A list of target audience segments.
|
||||
tone (str): The desired tone of the description (e.g., "Formal", "Informal").
|
||||
length (str): The desired length of the description (e.g., "short", "medium", "long").
|
||||
keywords (str): Keywords related to the product (comma-separated).
|
||||
|
||||
Returns:
|
||||
str: The generated product description.
|
||||
"""
|
||||
prompt = f"""
|
||||
Write a compelling product description for {title}.
|
||||
|
||||
Highlight these key features: {', '.join(details)}
|
||||
|
||||
Emphasize the benefits of these features for the target audience ({audience}).
|
||||
Maintain a {tone} tone and aim for a length of approximately {length} words.
|
||||
|
||||
Use these keywords naturally throughout the description: {', '.join(keywords)}.
|
||||
|
||||
Remember to be persuasive and focus on the value proposition.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Exit: Failed to get response from LLM: {err}")
|
||||
exit(1)
|
||||
|
||||
|
||||
def display_inputs():
|
||||
st.title("📝 AI Product Description Writer 🚀")
|
||||
st.markdown("**Generate compelling and accurate product descriptions with AI.**")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
product_title = st.text_input("🏷️ **Product Title**", placeholder="Enter the product title (e.g., Wireless Bluetooth Headphones)")
|
||||
with col2:
|
||||
product_details = st.text_area("📄 **Product Details**", placeholder="Enter features, benefits, specifications, materials, etc. (e.g., Noise Cancellation, Long Battery Life, Water Resistant, Comfortable Design)")
|
||||
|
||||
col3, col4 = st.columns(2)
|
||||
|
||||
with col3:
|
||||
keywords = st.text_input("🔑 **Keywords**", placeholder="Enter keywords, comma-separated (e.g., wireless headphones, noise cancelling, Bluetooth 5.0)")
|
||||
with col4:
|
||||
target_audience = st.multiselect(
|
||||
"🎯 **Target Audience**",
|
||||
["Teens", "Adults", "Seniors", "Music Lovers", "Fitness Enthusiasts", "Tech Savvy", "Busy Professionals", "Travelers", "Casual Users"],
|
||||
placeholder="Select target audience (optional)"
|
||||
)
|
||||
|
||||
col5, col6 = st.columns(2)
|
||||
|
||||
with col5:
|
||||
description_length = st.selectbox(
|
||||
"📏 **Desired Description Length**",
|
||||
["Short (1-2 sentences)", "Medium (3-5 sentences)", "Long (6+ sentences)"],
|
||||
help="Select the desired length of the product description"
|
||||
)
|
||||
with col6:
|
||||
brand_tone = st.selectbox(
|
||||
"🎨 **Brand Tone**",
|
||||
["Formal", "Informal", "Fun & Energetic"],
|
||||
help="Select the desired tone for the description"
|
||||
)
|
||||
|
||||
return product_title, product_details, target_audience, brand_tone, description_length, keywords
|
||||
|
||||
|
||||
def display_output(description):
|
||||
if description:
|
||||
st.subheader("✨ Generated Product Description:")
|
||||
st.write(description)
|
||||
|
||||
json_ld = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": product_title,
|
||||
"description": description,
|
||||
"audience": target_audience,
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": "Your Brand Name"
|
||||
},
|
||||
"keywords": keywords.split(", ")
|
||||
}
|
||||
|
||||
|
||||
def write_ai_prod_desc():
|
||||
product_title, product_details, target_audience, brand_tone, description_length, keywords = display_inputs()
|
||||
|
||||
if st.button("Generate Product Description 🚀"):
|
||||
with st.spinner("Generating description..."):
|
||||
description = generate_product_description(
|
||||
product_title,
|
||||
product_details.split(", "), # Split details into a list
|
||||
target_audience,
|
||||
brand_tone,
|
||||
description_length.split(" ")[0].lower(), # Extract length from selectbox
|
||||
keywords
|
||||
)
|
||||
display_output(description)
|
||||
@@ -1,75 +0,0 @@
|
||||
# AI Story Illustrator
|
||||
|
||||
The AI Story Illustrator is a powerful tool that generates beautiful illustrations for stories using Google's Gemini AI. This module allows users to input stories via text, file upload, or URL, and automatically generates appropriate illustrations for different scenes in the story.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Input Methods**: Input stories via direct text entry, file upload, or URL extraction
|
||||
- **Intelligent Scene Segmentation**: Automatically divides stories into logical segments for illustration
|
||||
- **Customizable Illustration Styles**: Choose from various artistic styles or define your own
|
||||
- **Scene Element Extraction**: Analyzes story segments to identify key visual elements
|
||||
- **Multiple Export Options**: Export as PDF storybook or ZIP archive of individual images
|
||||
- **Customizable Aspect Ratios**: Support for different image dimensions (16:9, 4:3, 1:1)
|
||||
- **Advanced Settings**: Control the number of segments to illustrate and other parameters
|
||||
|
||||
## Usage
|
||||
|
||||
The Story Illustrator is integrated into the Alwrity platform and can be accessed through the main interface. The workflow consists of three main steps:
|
||||
|
||||
1. **Story Input**: Enter your story text, upload a file, or provide a URL
|
||||
2. **Illustration Settings**: Configure the style, aspect ratio, and other parameters
|
||||
3. **Generate & Export**: Generate illustrations for all or individual segments and export the results
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Streamlit: For the user interface
|
||||
- Gemini AI: For image generation
|
||||
- BeautifulSoup: For URL text extraction
|
||||
- ReportLab: For PDF generation (optional)
|
||||
- PIL: For image processing
|
||||
|
||||
### Key Functions
|
||||
|
||||
- `segment_story()`: Divides a story into logical segments for illustration
|
||||
- `extract_scene_elements()`: Analyzes story segments to identify key visual elements
|
||||
- `generate_illustration_prompt()`: Creates detailed prompts for the AI image generator
|
||||
- `create_illustration()`: Generates an illustration for a story segment
|
||||
- `create_storybook_pdf()`: Combines story text and illustrations into a PDF
|
||||
- `create_zip_archive()`: Creates a ZIP archive of individual illustrations
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
from lib.ai_writers.ai_story_illustrator.story_illustrator import write_story_illustrator
|
||||
|
||||
# Run the Story Illustrator app
|
||||
write_story_illustrator()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Provide Clear Segments**: The system works best with stories that have clear scene transitions
|
||||
- **Be Specific with Styles**: More specific style descriptions yield better results
|
||||
- **Balance Text and Images**: For best results, aim for segments of 100-500 words per illustration
|
||||
- **Review and Regenerate**: If an illustration doesn't capture the scene well, use the regenerate option
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Support for more export formats (EPUB, HTML)
|
||||
- Enhanced character consistency across illustrations
|
||||
- Animation options for digital storytelling
|
||||
- Voice narration integration
|
||||
- Custom character design options
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If illustrations are not generating, check your internet connection and API access
|
||||
- If PDF export fails, ensure ReportLab is installed (`pip install reportlab`)
|
||||
- If URL extraction fails, try copying the text manually
|
||||
- For large stories, consider processing in smaller batches
|
||||
|
||||
## Credits
|
||||
|
||||
This module uses Google's Gemini AI for image generation and leverages various open-source libraries for text processing and document generation.
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
AI Story Illustrator module for generating illustrations for stories using AI.
|
||||
"""
|
||||
|
||||
from .story_illustrator import write_story_illustrator
|
||||
|
||||
__all__ = ['write_story_illustrator']
|
||||
@@ -1,727 +0,0 @@
|
||||
"""
|
||||
AI Story Illustrator - Generate illustrations for stories using Gemini AI
|
||||
|
||||
This module provides functionality to generate illustrations for stories using Google's Gemini AI.
|
||||
Users can input stories via text, file upload, or URL, and the system will generate appropriate
|
||||
illustrations for different scenes in the story.
|
||||
|
||||
Based on: https://github.com/google-gemini/cookbook/blob/main/examples/Book_illustration.ipynb
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import tempfile
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import io
|
||||
import base64
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
from bs4 import BeautifulSoup
|
||||
import zipfile
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger('story_illustrator')
|
||||
|
||||
# Constants
|
||||
MAX_STORY_LENGTH = 10000 # Maximum story length in characters
|
||||
MIN_SEGMENT_LENGTH = 100 # Minimum segment length for illustration
|
||||
MAX_SEGMENTS = 20 # Maximum number of segments to illustrate
|
||||
DEFAULT_STYLE = "digital art" # Default illustration style
|
||||
DEFAULT_ASPECT_RATIO = "16:9" # Default aspect ratio
|
||||
|
||||
|
||||
def extract_text_from_url(url):
|
||||
"""Extract text content from a URL."""
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
}
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
# Remove script and style elements
|
||||
for script in soup(["script", "style"]):
|
||||
script.extract()
|
||||
|
||||
# Get text
|
||||
text = soup.get_text(separator='\\n')
|
||||
|
||||
# Break into lines and remove leading and trailing space on each
|
||||
lines = (line.strip() for line in text.splitlines())
|
||||
# Break multi-headlines into a line each
|
||||
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
||||
# Drop blank lines
|
||||
text = '\\n'.join(chunk for chunk in chunks if chunk)
|
||||
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting text from URL: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def segment_story(story_text, min_segment_length=MIN_SEGMENT_LENGTH, max_segments=MAX_SEGMENTS):
|
||||
"""
|
||||
Segment a story into logical parts for illustration.
|
||||
Uses paragraph breaks, scene changes, and other indicators to create segments.
|
||||
"""
|
||||
# Clean up the text
|
||||
story_text = story_text.strip()
|
||||
|
||||
# Split by paragraphs first
|
||||
paragraphs = re.split(r'\\n\s*\\n', story_text)
|
||||
|
||||
# Initialize segments
|
||||
segments = []
|
||||
current_segment = ""
|
||||
|
||||
for paragraph in paragraphs:
|
||||
# Skip empty paragraphs
|
||||
if not paragraph.strip():
|
||||
continue
|
||||
|
||||
# If adding this paragraph would make the segment too long, start a new segment
|
||||
if len(current_segment) + len(paragraph) > 1000: # Limit segment size
|
||||
if current_segment:
|
||||
segments.append(current_segment.strip())
|
||||
current_segment = paragraph
|
||||
else:
|
||||
# Add paragraph to current segment
|
||||
if current_segment:
|
||||
current_segment += "\\n\\n" + paragraph
|
||||
else:
|
||||
current_segment = paragraph
|
||||
|
||||
# Add the last segment if it exists
|
||||
if current_segment:
|
||||
segments.append(current_segment.strip())
|
||||
|
||||
# Combine very short segments
|
||||
i = 0
|
||||
while i < len(segments) - 1:
|
||||
if len(segments[i]) < min_segment_length:
|
||||
segments[i] += "\\n\\n" + segments[i+1]
|
||||
segments.pop(i+1)
|
||||
else:
|
||||
i += 1
|
||||
|
||||
# Limit the number of segments
|
||||
if len(segments) > max_segments:
|
||||
# Combine segments to reduce the total number
|
||||
new_segments = []
|
||||
segment_size = len(segments) / max_segments
|
||||
|
||||
for i in range(max_segments):
|
||||
start_idx = int(i * segment_size)
|
||||
end_idx = int((i + 1) * segment_size)
|
||||
combined_segment = "\\n\\n".join(segments[start_idx:end_idx])
|
||||
new_segments.append(combined_segment)
|
||||
|
||||
segments = new_segments
|
||||
|
||||
return segments
|
||||
|
||||
|
||||
def extract_scene_elements(segment):
|
||||
"""
|
||||
Extract key scene elements from a story segment using LLM.
|
||||
This helps create more accurate illustration prompts.
|
||||
"""
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
prompt = f"""
|
||||
Analyze the following story segment and extract key visual elements for an illustration:
|
||||
|
||||
{segment}
|
||||
|
||||
Please provide:
|
||||
1. Main characters present (with brief visual descriptions)
|
||||
2. Setting/location details
|
||||
3. Key action or emotional moment to illustrate
|
||||
4. Important objects or props
|
||||
5. Time of day and lighting
|
||||
6. Weather or atmospheric conditions (if applicable)
|
||||
|
||||
Format your response as JSON with these keys: "characters", "setting", "key_moment", "objects", "lighting", "atmosphere"
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
|
||||
# Try to extract JSON from the response
|
||||
try:
|
||||
# Find JSON content between triple backticks if present
|
||||
json_match = re.search(r'```json\s*(.*?)\s*```', response, re.DOTALL)
|
||||
if json_match:
|
||||
json_str = json_match.group(1)
|
||||
else:
|
||||
# Otherwise try to parse the whole response as JSON
|
||||
json_str = response
|
||||
|
||||
scene_elements = json.loads(json_str)
|
||||
return scene_elements
|
||||
except json.JSONDecodeError:
|
||||
# If JSON parsing fails, extract information using regex
|
||||
characters = re.search(r'"characters":\s*"([^"]*)"', response)
|
||||
setting = re.search(r'"setting":\s*"([^"]*)"', response)
|
||||
|
||||
return {
|
||||
"characters": characters.group(1) if characters else "",
|
||||
"setting": setting.group(1) if setting else "",
|
||||
"key_moment": "",
|
||||
"objects": "",
|
||||
"lighting": "",
|
||||
"atmosphere": ""
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting scene elements: {e}")
|
||||
return {
|
||||
"characters": "",
|
||||
"setting": "",
|
||||
"key_moment": "",
|
||||
"objects": "",
|
||||
"lighting": "",
|
||||
"atmosphere": ""
|
||||
}
|
||||
|
||||
|
||||
def generate_illustration_prompt(segment, style, characters=None, setting=None):
|
||||
"""
|
||||
Generate a prompt for the illustration based on the segment content.
|
||||
|
||||
Args:
|
||||
segment: The story segment to illustrate
|
||||
style: The artistic style for the illustration
|
||||
characters: Optional character descriptions
|
||||
setting: Optional setting description
|
||||
|
||||
Returns:
|
||||
A prompt string for the image generation model
|
||||
"""
|
||||
# Create a base prompt
|
||||
base_prompt = f"""
|
||||
Create a detailed illustration for the following story segment in {style} style:
|
||||
|
||||
{segment[:500]} # Limit segment length for prompt
|
||||
|
||||
The illustration should capture the key elements, mood, and action of this scene.
|
||||
"""
|
||||
|
||||
# Add character information if provided
|
||||
if characters:
|
||||
base_prompt += f"\\n\\nThe main characters in this scene are: {characters}"
|
||||
|
||||
# Add setting information if provided
|
||||
if setting:
|
||||
base_prompt += f"\\n\\nThe setting is: {setting}"
|
||||
|
||||
# Add style-specific instructions
|
||||
if "watercolor" in style.lower():
|
||||
base_prompt += "\\n\\nUse soft, flowing watercolor techniques with visible brush strokes and color blending."
|
||||
elif "digital art" in style.lower():
|
||||
base_prompt += "\\n\\nCreate a polished digital illustration with clean lines and vibrant colors."
|
||||
elif "pencil sketch" in style.lower():
|
||||
base_prompt += "\\n\\nUse pencil sketch techniques with visible hatching, shading, and line work."
|
||||
|
||||
# Add final quality instructions
|
||||
base_prompt += """
|
||||
|
||||
Make the illustration:
|
||||
- Visually engaging and detailed
|
||||
- Appropriate for a storybook
|
||||
- Focused on the main action or emotion of the scene
|
||||
- With good composition and visual storytelling
|
||||
"""
|
||||
|
||||
return base_prompt.strip()
|
||||
|
||||
|
||||
def create_illustration(segment, style, aspect_ratio="16:9"):
|
||||
"""
|
||||
Create an illustration for a story segment.
|
||||
|
||||
Args:
|
||||
segment: The story segment to illustrate
|
||||
style: The artistic style for the illustration
|
||||
aspect_ratio: The aspect ratio for the illustration
|
||||
|
||||
Returns:
|
||||
Path to the generated image
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from ...gpt_providers.text_to_image_generation.gen_gemini_images import generate_gemini_image
|
||||
|
||||
# Extract scene elements to enhance the prompt
|
||||
scene_elements = extract_scene_elements(segment)
|
||||
|
||||
# Create a detailed prompt for the illustration
|
||||
prompt = generate_illustration_prompt(
|
||||
segment,
|
||||
style,
|
||||
characters=scene_elements.get("characters", ""),
|
||||
setting=scene_elements.get("setting", "")
|
||||
)
|
||||
|
||||
# Add key elements to the prompt
|
||||
key_moment = scene_elements.get("key_moment", "")
|
||||
objects = scene_elements.get("objects", "")
|
||||
lighting = scene_elements.get("lighting", "")
|
||||
atmosphere = scene_elements.get("atmosphere", "")
|
||||
|
||||
if key_moment:
|
||||
prompt += f"\\n\\nFocus on this key moment: {key_moment}"
|
||||
|
||||
if objects:
|
||||
prompt += f"\\n\\nInclude these important objects: {objects}"
|
||||
|
||||
if lighting:
|
||||
prompt += f"\\n\\nThe lighting is: {lighting}"
|
||||
|
||||
if atmosphere:
|
||||
prompt += f"\\n\\nThe atmosphere/weather is: {atmosphere}"
|
||||
|
||||
# Generate the illustration
|
||||
try:
|
||||
# Parse aspect ratio
|
||||
if aspect_ratio == "16:9":
|
||||
width, height = 16, 9
|
||||
elif aspect_ratio == "4:3":
|
||||
width, height = 4, 3
|
||||
elif aspect_ratio == "1:1":
|
||||
width, height = 1, 1
|
||||
else:
|
||||
width, height = 16, 9 # Default
|
||||
|
||||
# Generate image using Gemini
|
||||
image_path = generate_gemini_image(
|
||||
prompt=prompt,
|
||||
style=style.lower() if style else None,
|
||||
aspect_ratio=aspect_ratio
|
||||
)
|
||||
|
||||
return image_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating illustration: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create_storybook_pdf(segments, illustrations, title, author, output_path):
|
||||
"""
|
||||
Create a PDF storybook with text and illustrations.
|
||||
|
||||
Args:
|
||||
segments: List of story segments
|
||||
illustrations: List of paths to illustrations
|
||||
title: Book title
|
||||
author: Book author
|
||||
output_path: Path to save the PDF
|
||||
|
||||
Returns:
|
||||
Path to the created PDF
|
||||
"""
|
||||
try:
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image as ReportLabImage, PageBreak
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch
|
||||
|
||||
# Create a PDF document
|
||||
doc = SimpleDocTemplate(output_path, pagesize=A4)
|
||||
story = []
|
||||
|
||||
# Get styles
|
||||
styles = getSampleStyleSheet()
|
||||
title_style = styles['Title']
|
||||
author_style = styles['Normal']
|
||||
author_style.alignment = 1 # Center alignment
|
||||
normal_style = styles['Normal']
|
||||
|
||||
# Add title page
|
||||
story.append(Paragraph(title, title_style))
|
||||
story.append(Spacer(1, 0.5*inch))
|
||||
story.append(Paragraph(f"by {author}", author_style))
|
||||
story.append(PageBreak())
|
||||
|
||||
# Add content pages
|
||||
for i, (segment, illustration_path) in enumerate(zip(segments, illustrations)):
|
||||
if illustration_path and os.path.exists(illustration_path):
|
||||
# Add illustration
|
||||
img = ReportLabImage(illustration_path, width=6*inch, height=4*inch)
|
||||
story.append(img)
|
||||
story.append(Spacer(1, 0.25*inch))
|
||||
|
||||
# Add text
|
||||
for paragraph in segment.split('\\n\\n'):
|
||||
if paragraph.strip():
|
||||
story.append(Paragraph(paragraph, normal_style))
|
||||
story.append(Spacer(1, 0.1*inch))
|
||||
|
||||
# Add page break between segments
|
||||
if i < len(segments) - 1:
|
||||
story.append(PageBreak())
|
||||
|
||||
# Build the PDF
|
||||
doc.build(story)
|
||||
return output_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating PDF: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create_zip_archive(files, output_path):
|
||||
"""
|
||||
Create a ZIP archive containing the provided files.
|
||||
|
||||
Args:
|
||||
files: Dictionary of {filename: file_path} to include in the archive
|
||||
output_path: Path to save the ZIP file
|
||||
|
||||
Returns:
|
||||
Path to the created ZIP file
|
||||
"""
|
||||
try:
|
||||
with zipfile.ZipFile(output_path, 'w') as zipf:
|
||||
for filename, file_path in files.items():
|
||||
if os.path.exists(file_path):
|
||||
zipf.write(file_path, arcname=filename)
|
||||
return output_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating ZIP archive: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def write_story_illustrator():
|
||||
"""Main function for the Story Illustrator Streamlit app."""
|
||||
st.title("AI Story Illustrator")
|
||||
st.write("Generate beautiful illustrations for your stories using AI")
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3 = st.tabs(["Story Input", "Illustration Settings", "Generate & Export"])
|
||||
|
||||
# Initialize session state variables if they don't exist
|
||||
if "story_text" not in st.session_state:
|
||||
st.session_state.story_text = ""
|
||||
if "segments" not in st.session_state:
|
||||
st.session_state.segments = []
|
||||
if "illustrations" not in st.session_state:
|
||||
st.session_state.illustrations = []
|
||||
if "book_title" not in st.session_state:
|
||||
st.session_state.book_title = ""
|
||||
if "book_author" not in st.session_state:
|
||||
st.session_state.book_author = ""
|
||||
if "illustration_style" not in st.session_state:
|
||||
st.session_state.illustration_style = DEFAULT_STYLE
|
||||
if "aspect_ratio" not in st.session_state:
|
||||
st.session_state.aspect_ratio = DEFAULT_ASPECT_RATIO
|
||||
if "temp_files" not in st.session_state:
|
||||
st.session_state.temp_files = []
|
||||
|
||||
# Tab 1: Story Input
|
||||
with tab1:
|
||||
st.header("Step 1: Input Your Story")
|
||||
|
||||
# Input method selection
|
||||
input_method = st.radio(
|
||||
"Choose input method:",
|
||||
["Text Input", "File Upload", "URL"]
|
||||
)
|
||||
|
||||
if input_method == "Text Input":
|
||||
st.session_state.story_text = st.text_area(
|
||||
"Enter your story text:",
|
||||
value=st.session_state.story_text,
|
||||
height=300,
|
||||
max_chars=MAX_STORY_LENGTH,
|
||||
help="Enter the story text you want to illustrate (max 10,000 characters)"
|
||||
)
|
||||
|
||||
elif input_method == "File Upload":
|
||||
uploaded_file = st.file_uploader("Upload a text file:", type=["txt", "md"])
|
||||
if uploaded_file is not None:
|
||||
try:
|
||||
st.session_state.story_text = uploaded_file.getvalue().decode("utf-8")
|
||||
st.success(f"Successfully loaded file: {uploaded_file.name}")
|
||||
st.text_area("Preview:", value=st.session_state.story_text[:500] + "...", height=200, disabled=True)
|
||||
except Exception as e:
|
||||
st.error(f"Error reading file: {e}")
|
||||
|
||||
elif input_method == "URL":
|
||||
url = st.text_input("Enter URL containing the story:")
|
||||
if url:
|
||||
if st.button("Extract Text from URL"):
|
||||
with st.spinner("Extracting text from URL..."):
|
||||
extracted_text = extract_text_from_url(url)
|
||||
if extracted_text:
|
||||
st.session_state.story_text = extracted_text
|
||||
st.success("Successfully extracted text from URL")
|
||||
st.text_area("Preview:", value=st.session_state.story_text[:500] + "...", height=200, disabled=True)
|
||||
else:
|
||||
st.error("Failed to extract text from URL")
|
||||
|
||||
# Book metadata
|
||||
st.subheader("Book Metadata")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.session_state.book_title = st.text_input("Book Title:", value=st.session_state.book_title)
|
||||
with col2:
|
||||
st.session_state.book_author = st.text_input("Author:", value=st.session_state.book_author)
|
||||
|
||||
# Process story into segments
|
||||
if st.session_state.story_text:
|
||||
if st.button("Process Story into Segments"):
|
||||
with st.spinner("Processing story into segments..."):
|
||||
st.session_state.segments = segment_story(st.session_state.story_text)
|
||||
st.success(f"Story processed into {len(st.session_state.segments)} segments")
|
||||
|
||||
# Initialize illustrations list with None values
|
||||
st.session_state.illustrations = [None] * len(st.session_state.segments)
|
||||
|
||||
# Display segments
|
||||
st.subheader("Story Segments")
|
||||
for i, segment in enumerate(st.session_state.segments):
|
||||
with st.expander(f"Segment {i+1}"):
|
||||
st.write(segment)
|
||||
|
||||
# Tab 2: Illustration Settings
|
||||
with tab2:
|
||||
st.header("Step 2: Configure Illustration Settings")
|
||||
|
||||
# Style selection
|
||||
st.subheader("Illustration Style")
|
||||
style_options = [
|
||||
"Digital Art",
|
||||
"Watercolor Painting",
|
||||
"Pencil Sketch",
|
||||
"Oil Painting",
|
||||
"Cartoon",
|
||||
"Anime",
|
||||
"3D Render",
|
||||
"Pixel Art",
|
||||
"Children's Book Illustration",
|
||||
"Comic Book Style",
|
||||
"Fantasy Art",
|
||||
"Realistic"
|
||||
]
|
||||
|
||||
st.session_state.illustration_style = st.selectbox(
|
||||
"Choose an illustration style:",
|
||||
style_options,
|
||||
index=style_options.index(st.session_state.illustration_style) if st.session_state.illustration_style in style_options else 0
|
||||
)
|
||||
|
||||
# Custom style input
|
||||
use_custom_style = st.checkbox("Use custom style")
|
||||
if use_custom_style:
|
||||
custom_style = st.text_input("Describe your custom style:",
|
||||
placeholder="e.g., Impressionist painting with vibrant colors and visible brushstrokes")
|
||||
if custom_style:
|
||||
st.session_state.illustration_style = custom_style
|
||||
|
||||
# Display style examples
|
||||
st.info("💡 The style you choose will significantly impact the look and feel of your illustrations.")
|
||||
|
||||
# Aspect ratio selection
|
||||
st.subheader("Image Settings")
|
||||
aspect_ratio_options = {
|
||||
"16:9 (Widescreen)": "16:9",
|
||||
"4:3 (Standard)": "4:3",
|
||||
"1:1 (Square)": "1:1"
|
||||
}
|
||||
|
||||
selected_ratio = st.selectbox(
|
||||
"Choose aspect ratio:",
|
||||
list(aspect_ratio_options.keys()),
|
||||
index=list(aspect_ratio_options.values()).index(st.session_state.aspect_ratio) if st.session_state.aspect_ratio in aspect_ratio_options.values() else 0
|
||||
)
|
||||
st.session_state.aspect_ratio = aspect_ratio_options[selected_ratio]
|
||||
|
||||
# Advanced settings
|
||||
with st.expander("Advanced Settings"):
|
||||
st.slider("Number of segments to illustrate:", 1,
|
||||
max(len(st.session_state.segments), 1) if st.session_state.segments else 1,
|
||||
min(len(st.session_state.segments), MAX_SEGMENTS) if st.session_state.segments else 1,
|
||||
key="num_segments_to_illustrate")
|
||||
|
||||
st.checkbox("Generate cover image", value=True, key="generate_cover")
|
||||
|
||||
st.checkbox("Add text to illustrations", value=False, key="add_text_to_illustrations")
|
||||
|
||||
# Tab 3: Generate & Export
|
||||
with tab3:
|
||||
st.header("Step 3: Generate Illustrations & Export")
|
||||
|
||||
if not st.session_state.segments:
|
||||
st.warning("Please process your story into segments in Step 1 before generating illustrations.")
|
||||
else:
|
||||
# Generate illustrations
|
||||
st.subheader("Generate Illustrations")
|
||||
|
||||
num_segments = min(len(st.session_state.segments), st.session_state.get("num_segments_to_illustrate", len(st.session_state.segments)))
|
||||
|
||||
if st.button("Generate All Illustrations"):
|
||||
with st.spinner(f"Generating {num_segments} illustrations... This may take a while."):
|
||||
progress_bar = st.progress(0)
|
||||
|
||||
for i in range(num_segments):
|
||||
# Update progress
|
||||
progress_bar.progress((i) / num_segments)
|
||||
st.write(f"Generating illustration {i+1} of {num_segments}...")
|
||||
|
||||
# Generate illustration
|
||||
illustration_path = create_illustration(
|
||||
st.session_state.segments[i],
|
||||
st.session_state.illustration_style,
|
||||
st.session_state.aspect_ratio
|
||||
)
|
||||
|
||||
# Store the illustration path
|
||||
if illustration_path:
|
||||
st.session_state.illustrations[i] = illustration_path
|
||||
st.session_state.temp_files.append(illustration_path)
|
||||
|
||||
# Complete progress
|
||||
progress_bar.progress(1.0)
|
||||
st.success(f"Generated {num_segments} illustrations!")
|
||||
|
||||
# Generate individual illustrations
|
||||
st.subheader("Generate Individual Illustrations")
|
||||
|
||||
for i in range(num_segments):
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
with st.expander(f"Segment {i+1}"):
|
||||
st.write(st.session_state.segments[i][:300] + "..." if len(st.session_state.segments[i]) > 300 else st.session_state.segments[i])
|
||||
|
||||
with col2:
|
||||
if st.button(f"Generate #{i+1}", key=f"gen_btn_{i}"):
|
||||
with st.spinner(f"Generating illustration {i+1}..."):
|
||||
illustration_path = create_illustration(
|
||||
st.session_state.segments[i],
|
||||
st.session_state.illustration_style,
|
||||
st.session_state.aspect_ratio
|
||||
)
|
||||
|
||||
if illustration_path:
|
||||
st.session_state.illustrations[i] = illustration_path
|
||||
st.session_state.temp_files.append(illustration_path)
|
||||
st.success(f"Generated illustration {i+1}!")
|
||||
|
||||
# Display generated illustrations
|
||||
st.subheader("Preview Illustrations")
|
||||
|
||||
if any(st.session_state.illustrations):
|
||||
for i, illustration_path in enumerate(st.session_state.illustrations[:num_segments]):
|
||||
if illustration_path and os.path.exists(illustration_path):
|
||||
with st.expander(f"Illustration {i+1}"):
|
||||
st.image(illustration_path, caption=f"Illustration for Segment {i+1}", use_column_width=True)
|
||||
|
||||
# Regenerate button
|
||||
if st.button(f"Regenerate", key=f"regen_btn_{i}"):
|
||||
with st.spinner(f"Regenerating illustration {i+1}..."):
|
||||
new_illustration_path = create_illustration(
|
||||
st.session_state.segments[i],
|
||||
st.session_state.illustration_style,
|
||||
st.session_state.aspect_ratio
|
||||
)
|
||||
|
||||
if new_illustration_path:
|
||||
st.session_state.illustrations[i] = new_illustration_path
|
||||
st.session_state.temp_files.append(new_illustration_path)
|
||||
st.rerun()
|
||||
else:
|
||||
st.info("No illustrations generated yet. Click 'Generate All Illustrations' or generate individual illustrations.")
|
||||
|
||||
# Export options
|
||||
st.subheader("Export Options")
|
||||
|
||||
if any(st.session_state.illustrations):
|
||||
export_format = st.radio(
|
||||
"Export format:",
|
||||
["PDF Storybook", "Individual Images (ZIP)", "Both"]
|
||||
)
|
||||
|
||||
if st.button("Export"):
|
||||
with st.spinner("Preparing export..."):
|
||||
# Create temporary directory for exports
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Filter out None values from illustrations
|
||||
valid_illustrations = [path for path in st.session_state.illustrations[:num_segments] if path and os.path.exists(path)]
|
||||
valid_segments = st.session_state.segments[:len(valid_illustrations)]
|
||||
|
||||
# Prepare filenames
|
||||
safe_title = "".join(c if c.isalnum() else "_" for c in st.session_state.book_title) if st.session_state.book_title else "story"
|
||||
timestamp = int(time.time())
|
||||
|
||||
# Export as PDF
|
||||
if export_format in ["PDF Storybook", "Both"]:
|
||||
pdf_path = os.path.join(temp_dir, f"{safe_title}_{timestamp}.pdf")
|
||||
|
||||
try:
|
||||
pdf_result = create_storybook_pdf(
|
||||
valid_segments,
|
||||
valid_illustrations,
|
||||
st.session_state.book_title or "Untitled Story",
|
||||
st.session_state.book_author or "Anonymous",
|
||||
pdf_path
|
||||
)
|
||||
|
||||
if pdf_result:
|
||||
with open(pdf_path, "rb") as f:
|
||||
st.download_button(
|
||||
label="Download PDF Storybook",
|
||||
data=f,
|
||||
file_name=f"{safe_title}.pdf",
|
||||
mime="application/pdf"
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Error creating PDF: {e}")
|
||||
st.info("Please install ReportLab to enable PDF export: pip install reportlab")
|
||||
|
||||
# Export as ZIP of images
|
||||
if export_format in ["Individual Images (ZIP)", "Both"]:
|
||||
zip_path = os.path.join(temp_dir, f"{safe_title}_illustrations_{timestamp}.zip")
|
||||
|
||||
# Prepare files for ZIP
|
||||
files_to_zip = {}
|
||||
for i, img_path in enumerate(valid_illustrations):
|
||||
if img_path and os.path.exists(img_path):
|
||||
files_to_zip[f"illustration_{i+1}.png"] = img_path
|
||||
|
||||
zip_result = create_zip_archive(files_to_zip, zip_path)
|
||||
|
||||
if zip_result:
|
||||
with open(zip_path, "rb") as f:
|
||||
st.download_button(
|
||||
label="Download Illustrations ZIP",
|
||||
data=f,
|
||||
file_name=f"{safe_title}_illustrations.zip",
|
||||
mime="application/zip"
|
||||
)
|
||||
else:
|
||||
st.info("Generate illustrations before exporting.")
|
||||
|
||||
# Cleanup temporary files when the session ends
|
||||
def cleanup_temp_files():
|
||||
for file_path in st.session_state.temp_files:
|
||||
try:
|
||||
if file_path and os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing temporary file {file_path}: {e}")
|
||||
|
||||
# Register the cleanup function to run when the session ends
|
||||
import atexit
|
||||
atexit.register(cleanup_temp_files)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
write_story_illustrator()
|
||||
@@ -1,450 +0,0 @@
|
||||
"""
|
||||
Utility functions for the AI Story Illustrator module.
|
||||
|
||||
This module provides helper functions for file operations, string manipulation,
|
||||
and simple text analysis relevant to story processing.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional, Union
|
||||
|
||||
# Attempt to import Pillow for image dimensions, but don't fail if not installed
|
||||
# unless the specific function is called.
|
||||
try:
|
||||
from PIL import Image
|
||||
_PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
_PIL_AVAILABLE = False
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger('story_illustrator_utils')
|
||||
|
||||
# --- Constants ---
|
||||
IMAGE_EXTENSIONS = frozenset(['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'])
|
||||
TEXT_EXTENSIONS = frozenset(['.txt', '.md', '.text'])
|
||||
# Common English words that often start sentences, excluded from simple name detection
|
||||
COMMON_START_WORDS = frozenset([
|
||||
'The', 'A', 'An', 'And', 'But', 'Or', 'For', 'Nor', 'So', 'Yet', 'He', 'She',
|
||||
'It', 'They', 'We', 'You', 'I', 'In', 'On', 'At', 'To', 'From', 'With',
|
||||
'About', 'As', 'Is', 'Was', 'Were', 'Be', 'Been', 'Being', 'Have', 'Has',
|
||||
'Had', 'Do', 'Does', 'Did', 'Will', 'Would', 'Shall', 'Should', 'May',
|
||||
'Might', 'Must', 'Can', 'Could'
|
||||
])
|
||||
|
||||
|
||||
# --- File/Directory Operations ---
|
||||
|
||||
def create_temp_directory(prefix: str = "story_illustrator_") -> str:
|
||||
"""
|
||||
Creates a temporary directory using tempfile.mkdtemp.
|
||||
|
||||
Args:
|
||||
prefix: A prefix for the temporary directory name.
|
||||
|
||||
Returns:
|
||||
The absolute path to the created temporary directory.
|
||||
"""
|
||||
try:
|
||||
temp_dir = tempfile.mkdtemp(prefix=prefix)
|
||||
logger.info(f"Created temporary directory: {temp_dir}")
|
||||
return temp_dir
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create temporary directory: {e}", exc_info=True)
|
||||
raise # Re-raise the exception after logging
|
||||
|
||||
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
"""
|
||||
Sanitizes a filename by removing/replacing invalid characters for common filesystems.
|
||||
|
||||
Args:
|
||||
filename: The original filename string.
|
||||
|
||||
Returns:
|
||||
A sanitized filename string suitable for use in file paths.
|
||||
"""
|
||||
if not isinstance(filename, str):
|
||||
logger.warning("sanitize_filename received non-string input, converting.")
|
||||
filename = str(filename)
|
||||
|
||||
# Remove characters invalid for Windows/Unix filenames
|
||||
# Replace them with an underscore.
|
||||
sanitized = re.sub(r'[\\/*?:"<>|\']', "_", filename)
|
||||
# Replace consecutive underscores/spaces with a single underscore
|
||||
sanitized = re.sub(r'[_ ]+', '_', sanitized)
|
||||
# Remove leading/trailing spaces, dots, and underscores
|
||||
sanitized = sanitized.strip("._ ")
|
||||
|
||||
# Ensure the filename is not empty after sanitization
|
||||
if not sanitized:
|
||||
sanitized = "unnamed_file"
|
||||
logger.warning("Filename was empty after sanitization, using default.")
|
||||
|
||||
# Limit filename length (optional, adjust as needed)
|
||||
# max_len = 255 # Example limit
|
||||
# if len(sanitized) > max_len:
|
||||
# name, ext = os.path.splitext(sanitized)
|
||||
# sanitized = name[:max_len - len(ext) - 1] + "_" + ext
|
||||
# logger.warning(f"Filename truncated to maximum length: {sanitized}")
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
def get_temp_file_path(
|
||||
directory: str, prefix: str = "file_", suffix: str = ".tmp"
|
||||
) -> str:
|
||||
"""
|
||||
Generates a unique temporary file path within the specified directory.
|
||||
|
||||
Args:
|
||||
directory: The directory where the temporary file should be located.
|
||||
prefix: A prefix for the filename.
|
||||
suffix: A suffix (extension) for the filename.
|
||||
|
||||
Returns:
|
||||
The full path for the unique temporary file.
|
||||
"""
|
||||
# Ensure suffix starts with a dot if it's meant to be an extension
|
||||
if suffix and not suffix.startswith("."):
|
||||
suffix = "." + suffix
|
||||
|
||||
unique_id = uuid.uuid4().hex[:12] # Longer hex UUID for better uniqueness
|
||||
filename = f"{prefix}{unique_id}{suffix}"
|
||||
return os.path.join(directory, filename)
|
||||
|
||||
|
||||
def ensure_directory_exists(directory: Union[str, Path]) -> str:
|
||||
"""
|
||||
Ensures that a directory exists, creating it recursively if necessary.
|
||||
|
||||
Args:
|
||||
directory: The path to the directory (string or Path object).
|
||||
|
||||
Returns:
|
||||
The absolute path to the directory as a string.
|
||||
|
||||
Raises:
|
||||
OSError: If the directory cannot be created (e.g., permission issues).
|
||||
"""
|
||||
dir_path = Path(directory).resolve() # Use Pathlib for robust handling
|
||||
try:
|
||||
dir_path.mkdir(parents=True, exist_ok=True)
|
||||
# Log only if it needed creation (or if verbose logging is on)
|
||||
# logger.info(f"Ensured directory exists: {dir_path}")
|
||||
return str(dir_path)
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to create or access directory {dir_path}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def cleanup_directory(directory: Union[str, Path]) -> None:
|
||||
"""
|
||||
Removes a directory and all its contents recursively. Handles errors gracefully.
|
||||
|
||||
Args:
|
||||
directory: The path to the directory to remove (string or Path object).
|
||||
"""
|
||||
dir_path = Path(directory)
|
||||
if not dir_path.exists():
|
||||
logger.debug(f"Cleanup skipped: Directory '{directory}' does not exist.")
|
||||
return
|
||||
|
||||
if not dir_path.is_dir():
|
||||
logger.warning(f"Cleanup warning: Path '{directory}' is not a directory.")
|
||||
return
|
||||
|
||||
try:
|
||||
shutil.rmtree(dir_path)
|
||||
logger.info(f"Successfully removed directory: {directory}")
|
||||
except OSError as e:
|
||||
logger.error(f"Error removing directory {directory}: {e}", exc_info=True)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Unexpected error removing directory {directory}: {e}", exc_info=True
|
||||
)
|
||||
|
||||
|
||||
# --- File Type Checks ---
|
||||
|
||||
def get_file_extension(file_path: Union[str, Path]) -> str:
|
||||
"""
|
||||
Gets the lowercased file extension (including the dot) from a file path.
|
||||
|
||||
Args:
|
||||
file_path: The path to the file (string or Path object).
|
||||
|
||||
Returns:
|
||||
The file extension (e.g., '.txt', '.png') or an empty string if no extension.
|
||||
"""
|
||||
return Path(file_path).suffix.lower()
|
||||
|
||||
|
||||
def is_image_file(file_path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Checks if a file is likely an image based on its extension.
|
||||
|
||||
Args:
|
||||
file_path: The path to the file (string or Path object).
|
||||
|
||||
Returns:
|
||||
True if the file extension is in IMAGE_EXTENSIONS, False otherwise.
|
||||
"""
|
||||
return get_file_extension(file_path) in IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
def is_text_file(file_path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Checks if a file is likely a text file based on its extension.
|
||||
|
||||
Args:
|
||||
file_path: The path to the file (string or Path object).
|
||||
|
||||
Returns:
|
||||
True if the file extension is in TEXT_EXTENSIONS, False otherwise.
|
||||
"""
|
||||
return get_file_extension(file_path) in TEXT_EXTENSIONS
|
||||
|
||||
|
||||
# --- Text Analysis (Simple Heuristics) ---
|
||||
|
||||
def extract_story_title_from_text(text: str) -> str:
|
||||
"""
|
||||
Attempts to extract a title from story text using simple heuristics.
|
||||
|
||||
Looks for patterns (in order):
|
||||
1. Markdown headers (#, ##, etc.) at the start of a line.
|
||||
2. The first non-empty line if it's short (< 100 chars) and followed by
|
||||
a blank line or is the only line.
|
||||
3. The first non-empty line if it's entirely in uppercase (< 100 chars).
|
||||
|
||||
Args:
|
||||
text: The story text content.
|
||||
|
||||
Returns:
|
||||
An extracted title string, or "Untitled Story" if no pattern matches.
|
||||
"""
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return "Untitled Story"
|
||||
|
||||
# 1. Check for markdown headers ( # Title, ## Title )
|
||||
# Needs to match start of line (^) with optional whitespace before #
|
||||
header_match = re.search(r'^\s*#+\s+(.+)$', text.strip(), re.MULTILINE)
|
||||
if header_match:
|
||||
title = header_match.group(1).strip()
|
||||
if title: return title
|
||||
|
||||
lines = text.strip().split('\n')
|
||||
if not lines:
|
||||
return "Untitled Story"
|
||||
|
||||
first_line = lines[0].strip()
|
||||
if not first_line: # Skip if first line is blank
|
||||
if len(lines) > 1:
|
||||
first_line = lines[1].strip() # Try second line
|
||||
else:
|
||||
return "Untitled Story"
|
||||
|
||||
if not first_line: # Still no title found
|
||||
return "Untitled Story"
|
||||
|
||||
# 2. Check if first line is short and potentially a title
|
||||
is_short = len(first_line) < 100
|
||||
is_followed_by_blank = len(lines) > 1 and not lines[1].strip()
|
||||
is_only_line = len(lines) == 1
|
||||
|
||||
if is_short and (is_followed_by_blank or is_only_line):
|
||||
return first_line
|
||||
|
||||
# 3. Check if first line is all caps (and short)
|
||||
is_all_caps = first_line == first_line.upper() and first_line.isalpha() # Check if it contains letters
|
||||
if is_short and is_all_caps:
|
||||
return first_line
|
||||
|
||||
# Default if no other pattern matched
|
||||
return "Untitled Story"
|
||||
|
||||
|
||||
def estimate_reading_time(text: str, words_per_minute: int = 200) -> float:
|
||||
"""
|
||||
Estimates the reading time of a text in minutes.
|
||||
|
||||
Args:
|
||||
text: The text content.
|
||||
words_per_minute: The assumed average reading speed.
|
||||
|
||||
Returns:
|
||||
The estimated reading time in minutes. Returns 0.0 for empty text.
|
||||
"""
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return 0.0
|
||||
if words_per_minute <= 0:
|
||||
raise ValueError("words_per_minute must be positive.")
|
||||
|
||||
word_count = len(text.split())
|
||||
minutes = word_count / words_per_minute
|
||||
return minutes
|
||||
|
||||
|
||||
def count_sentences(text: str) -> int:
|
||||
"""
|
||||
Counts the number of sentences in a text using a very simple heuristic.
|
||||
|
||||
Note: This is a basic implementation counting sentence-ending punctuation
|
||||
(. ! ?). It will be inaccurate with abbreviations (Mr., Mrs., etc.),
|
||||
ellipses, and complex sentence structures.
|
||||
|
||||
Args:
|
||||
text: The text content.
|
||||
|
||||
Returns:
|
||||
An estimated count of sentences. Returns 0 for empty text.
|
||||
"""
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return 0
|
||||
|
||||
# Find sequences of one or more sentence-ending punctuation marks
|
||||
sentence_endings = re.findall(r'[.!?]+', text)
|
||||
count = len(sentence_endings)
|
||||
|
||||
# Handle edge case where text might not end with punctuation but isn't empty
|
||||
if count == 0 and len(text.strip()) > 0:
|
||||
return 1 # Assume at least one sentence if text exists but no terminators found
|
||||
return count
|
||||
|
||||
|
||||
def extract_character_names(text: str, min_occurrences: int = 2) -> List[str]:
|
||||
"""
|
||||
Attempts to extract potential character names from story text.
|
||||
|
||||
Note: This is a simple heuristic based on finding capitalized words
|
||||
(excluding common sentence starters) that appear multiple times. It has
|
||||
limitations and may produce false positives or miss actual names.
|
||||
|
||||
Args:
|
||||
text: The story text content.
|
||||
min_occurrences: The minimum number of times a capitalized word must
|
||||
appear to be considered a potential name.
|
||||
|
||||
Returns:
|
||||
A list of potential character name strings.
|
||||
"""
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return []
|
||||
if min_occurrences < 1:
|
||||
min_occurrences = 1 # Ensure at least one occurrence is required
|
||||
|
||||
# Find words starting with an uppercase letter, potentially followed by lowercase
|
||||
# Allows for single-letter names like 'X' but focuses on typical Name structure
|
||||
capitalized_words = re.findall(r'\b[A-Z][a-zA-Z]*\b', text)
|
||||
|
||||
# Count occurrences, excluding common words
|
||||
word_counts: Dict[str, int] = {}
|
||||
for word in capitalized_words:
|
||||
if word not in COMMON_START_WORDS:
|
||||
word_counts[word] = word_counts.get(word, 0) + 1
|
||||
|
||||
# Filter for words that meet the minimum occurrence threshold
|
||||
potential_names = [
|
||||
word for word, count in word_counts.items() if count >= min_occurrences
|
||||
]
|
||||
|
||||
# Sort for consistency (optional)
|
||||
potential_names.sort()
|
||||
|
||||
return potential_names
|
||||
|
||||
|
||||
def extract_setting_details(text: str) -> List[str]:
|
||||
"""
|
||||
Attempts to extract potential setting details using simple regex patterns.
|
||||
|
||||
Note: This is a very basic heuristic looking for common prepositional
|
||||
phrases (e.g., "in the forest", "at the castle"). It is highly limited
|
||||
and likely to miss many setting details or extract irrelevant phrases.
|
||||
|
||||
Args:
|
||||
text: The story text content.
|
||||
|
||||
Returns:
|
||||
A list of potential setting phrases found.
|
||||
"""
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return []
|
||||
|
||||
# Patterns looking for prepositions followed by nouns/adjectives
|
||||
# Making patterns slightly more general:
|
||||
# (\b\w+\b) captures single words
|
||||
# (\b\w+\s+\w+\b) captures two-word phrases
|
||||
# (\b[A-Z]\w*\b) captures capitalized words (potential proper nouns)
|
||||
setting_patterns = [
|
||||
r'\b(?:in|on|at|near|beside|inside|outside|under|over|through)\s+(?:the|a|an)\s+((?:[A-Z]\w*|\w+)(?:\s+\w+){0,2})\b', # e.g., in the old house
|
||||
r'\b(?:in|on|at)\s+((?:[A-Z]\w+)(?:\s+[A-Z]\w+)*)\b', # e.g., in New York City
|
||||
r'\b(?:during|before|after)\s+(?:the|a|an)\s+(\w+(?:\s+\w+){0,2})\b', # e.g., during the storm
|
||||
]
|
||||
|
||||
settings_found = set() # Use a set to avoid duplicates
|
||||
for pattern in setting_patterns:
|
||||
try:
|
||||
matches = re.findall(pattern, text, re.IGNORECASE) # Ignore case
|
||||
for match in matches:
|
||||
# If match is tuple due to multiple capture groups, join them?
|
||||
# For these patterns, it should be single strings.
|
||||
if isinstance(match, str):
|
||||
phrase = match.strip()
|
||||
if phrase and len(phrase.split()) <= 5: # Limit phrase length
|
||||
settings_found.add(phrase)
|
||||
except re.error as e:
|
||||
logger.warning(f"Regex error in extract_setting_details: {e} with pattern: {pattern}")
|
||||
|
||||
|
||||
# Convert set back to list and sort for consistency
|
||||
sorted_settings = sorted(list(settings_found))
|
||||
return sorted_settings
|
||||
|
||||
|
||||
# --- Image Operations ---
|
||||
|
||||
def get_image_dimensions(image_path: Union[str, Path]) -> Optional[Tuple[int, int]]:
|
||||
"""
|
||||
Gets the (width, height) dimensions of an image file using Pillow.
|
||||
|
||||
Args:
|
||||
image_path: The path to the image file (string or Path object).
|
||||
|
||||
Returns:
|
||||
A tuple (width, height) if successful, or None if the file is not
|
||||
a valid image, Pillow is not installed, or an error occurs.
|
||||
"""
|
||||
if not _PIL_AVAILABLE:
|
||||
logger.warning("Pillow (PIL) library not installed. Cannot get image dimensions.")
|
||||
return None
|
||||
|
||||
img_path = Path(image_path)
|
||||
if not img_path.is_file():
|
||||
logger.error(f"Image file not found or is not a file: {image_path}")
|
||||
return None
|
||||
|
||||
try:
|
||||
with Image.open(img_path) as img:
|
||||
width, height = img.size
|
||||
logger.debug(f"Dimensions for {image_path}: {width}x{height}")
|
||||
return width, height
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Image file not found at path: {image_path}")
|
||||
return None
|
||||
except UnidentifiedImageError: # Specific Pillow error for invalid images
|
||||
logger.error(f"Could not identify image file (invalid format or corrupted): {image_path}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting dimensions for image {image_path}: {e}", exc_info=True)
|
||||
return None
|
||||
@@ -1,31 +0,0 @@
|
||||
# AI Story Video Generator
|
||||
|
||||
This module allows users to generate animated story videos using AI. It leverages Google's Gemini model to create stories and generate images for each scene, then combines them into a video.
|
||||
|
||||
## Features
|
||||
|
||||
- Generate complete stories based on user prompts
|
||||
- Create scene-by-scene storyboards
|
||||
- Generate images for each scene using Gemini
|
||||
- Compile images into an animated video
|
||||
- Add background music and text overlays
|
||||
- Export videos in MP4 format
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User provides a story prompt and preferences
|
||||
2. AI generates a complete story with multiple scenes
|
||||
3. For each scene, an image is generated
|
||||
4. Images are compiled into a video with transitions
|
||||
5. Optional background music and text overlays are added
|
||||
6. The final video is available for download
|
||||
|
||||
## Requirements
|
||||
|
||||
- Google Gemini API key
|
||||
- FFmpeg for video processing
|
||||
- Python libraries: moviepy, pillow, requests
|
||||
|
||||
## Usage
|
||||
|
||||
Access this tool through the Streamlit interface by selecting "AI Story Video Generator" from the main menu.
|
||||
@@ -1,4 +0,0 @@
|
||||
# AI Story Video Generator module
|
||||
from .story_video_generator import write_story_video_generator
|
||||
|
||||
__all__ = ["write_story_video_generator"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
"""
|
||||
Utility functions for the AI Story Video Generator.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# Constants
|
||||
TEMP_DIR = Path(tempfile.gettempdir()) / "alwrity_story_generator"
|
||||
|
||||
def ensure_temp_dir() -> Path:
|
||||
"""Ensure the temporary directory exists and return its path."""
|
||||
os.makedirs(TEMP_DIR, exist_ok=True)
|
||||
return TEMP_DIR
|
||||
|
||||
def get_temp_filepath(prefix: str, extension: str) -> str:
|
||||
"""Generate a temporary file path with the given prefix and extension."""
|
||||
temp_dir = ensure_temp_dir()
|
||||
return str(temp_dir / f"{prefix}_{uuid.uuid4()}.{extension}")
|
||||
|
||||
def clean_temp_files(older_than_hours: int = 24) -> int:
|
||||
"""
|
||||
Clean temporary files older than the specified number of hours.
|
||||
|
||||
Args:
|
||||
older_than_hours: Remove files older than this many hours
|
||||
|
||||
Returns:
|
||||
Number of files removed
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
temp_dir = ensure_temp_dir()
|
||||
cutoff_time = time.time() - (older_than_hours * 3600)
|
||||
count = 0
|
||||
|
||||
for file_path in temp_dir.glob("*"):
|
||||
if file_path.is_file() and file_path.stat().st_mtime < cutoff_time:
|
||||
try:
|
||||
file_path.unlink()
|
||||
count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return count
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""Format seconds into a MM:SS string."""
|
||||
minutes = int(seconds // 60)
|
||||
remaining_seconds = int(seconds % 60)
|
||||
return f"{minutes}:{remaining_seconds:02d}"
|
||||
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
"""Sanitize a string to be used as a filename."""
|
||||
import re
|
||||
# Remove invalid characters
|
||||
sanitized = re.sub(r'[^\w\s-]', '', filename)
|
||||
# Replace spaces with underscores
|
||||
sanitized = sanitized.strip().replace(' ', '_')
|
||||
return sanitized
|
||||
@@ -1,103 +0,0 @@
|
||||
# AI Story Generator App
|
||||
|
||||
In the age of AI, creativity and technology are intertwining in ways that are transforming how we tell stories. Imagine having the power to craft a captivating narrative tailored to your exact specifications with just a few clicks. Whether you're an aspiring writer, a seasoned novelist, or just someone who loves a good story, our new AI-powered story writing app is here to make storytelling easier and more engaging than ever before.
|
||||
|
||||
## Why an AI Story Writing App?
|
||||
|
||||
Storytelling has always been a cherished art form, but not everyone finds it easy to start from scratch. With the AI Story Generator App, you can create detailed and personalized stories by simply providing some key inputs. Our app uses advanced AI to turn your ideas into compelling narratives, helping you overcome writer's block and unleashing your creative potential.
|
||||
|
||||
## Features of the AI Story Generator App
|
||||
|
||||
### Genre
|
||||
Choose from a variety of genres such as Fantasy, Sci-Fi, Mystery, Romance, and Horror to set the tone for your story.
|
||||
|
||||
### Story Setting
|
||||
Provide a detailed setting for your story, including location and time period.
|
||||
|
||||
For example:
|
||||
A bustling futuristic city with towering skyscrapers and flying cars, set in the year 2150. The city is known for its technological advancements but has a dark underbelly of crime and corruption.
|
||||
|
||||
|
||||
### Main Characters
|
||||
Input the names, descriptions, and roles of your main characters.
|
||||
|
||||
For example:
|
||||
Character Names: John, Xishan, Amol
|
||||
Character Descriptions: John is a tall, muscular man with a kind heart. Xishan is a clever and resourceful woman. Amol is a mischievous and energetic young boy.
|
||||
Character Roles: John - Hero, Xishan - Sidekick, Amol - Supporting Character
|
||||
|
||||
|
||||
### Plot Elements
|
||||
Outline the key plot elements including the story theme, key events, and main conflict.
|
||||
|
||||
For example:
|
||||
Story Theme: Love conquers all, The hero's journey, Good vs. evil
|
||||
|
||||
Key Events or Plot Points:
|
||||
|
||||
The hero meets the villain
|
||||
The hero faces a challenge
|
||||
The hero overcomes the conflict
|
||||
Main Conflict or Problem:
|
||||
The hero must save the world from a powerful enemy, The hero must overcome a personal obstacle to achieve their goal.
|
||||
|
||||
|
||||
### Tone and Style
|
||||
Choose the writing style, tone, and narrative point of view for your story.
|
||||
|
||||
For example:
|
||||
Writing Style: Formal, Casual, Poetic, Humorous
|
||||
Story Tone: Dark
|
||||
|
||||
### Perspective
|
||||
Choose the narrative point of view from which the story is told (e.g., first person, third person limited, third person omniscient).
|
||||
|
||||
### Target Audience
|
||||
Specify the intended audience age group (Children, Young Adults, Adults) and set a content rating (G, PG, PG-13, R) for appropriateness.
|
||||
|
||||
### Ending Preference
|
||||
Select the type of ending you prefer for the story (e.g., happy, tragic, cliffhanger, twist).
|
||||
|
||||
## How to Use
|
||||
|
||||
Choose Genre: Select the genre that best fits your story idea.
|
||||
Set Story Setting: Describe the setting and time period where your story unfolds.
|
||||
Define Characters: Provide names, descriptions, and roles for your main characters.
|
||||
Outline Plot Elements: Detail the story's theme, key events, and main conflict.
|
||||
Select Tone and Style: Choose the writing style and tone that align with your story's mood.
|
||||
Specify Perspective: Decide on the narrative point of view.
|
||||
Target Audience: Specify the age group and content rating.
|
||||
Choose Ending: Select the preferred type of story conclusion.
|
||||
Generate Story: Click the "Generate Story" button to receive a customized story prompt based on your inputs.
|
||||
|
||||
|
||||
### Example Prompt
|
||||
|
||||
**Genre:** Fantasy
|
||||
**Setting:** A mystical forest in a medieval realm, where magic thrives and mythical creatures roam freely.
|
||||
**Characters:**
|
||||
- Name: Elara
|
||||
Description: Elara is a young elf with a mischievous glint in her emerald eyes, known for her ability to wield powerful spells.
|
||||
Role: Protagonist
|
||||
- Name: Thorne
|
||||
Description: Thorne is a gruff dwarf with a heart of gold, skilled in forging enchanted weapons.
|
||||
Role: Sidekick
|
||||
- Name: Malachai
|
||||
Description: Malachai is a cunning dragon with shimmering scales of azure, whose allegiance is uncertain.
|
||||
Role: Antagonist
|
||||
|
||||
**Plot Elements:**
|
||||
- Theme: The power of friendship and bravery in the face of adversity.
|
||||
- Key Events: Elara discovers an ancient prophecy that foretells a looming darkness threatening the realm. Thorne crafts a legendary sword to aid in their quest. Malachai challenges Elara's resolve, forcing her to make a difficult choice.
|
||||
- Conflict: Elara must gather allies and confront the dark sorcerer who seeks to plunge the realm into eternal shadow.
|
||||
|
||||
**Writing Style:** Poetic
|
||||
**Tone:** Whimsical
|
||||
**Point of View:** Third Person Limited
|
||||
|
||||
**Audience:** Young Adults, **Content Rating:** PG
|
||||
**Ending:** Happy
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
#####################################################
|
||||
#
|
||||
# google-gemini-cookbook - Story_Writing_with_Prompt_Chaining
|
||||
#
|
||||
#####################################################
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
import sys
|
||||
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def generate_with_retry(prompt, system_prompt=None):
|
||||
"""
|
||||
Generates content using the llm_text_gen function with retry handling for errors.
|
||||
|
||||
Parameters:
|
||||
prompt (str): The prompt to generate content from.
|
||||
system_prompt (str, optional): Custom system prompt to use instead of the default one.
|
||||
|
||||
Returns:
|
||||
str: The generated content.
|
||||
"""
|
||||
try:
|
||||
# Use llm_text_gen instead of directly calling the model
|
||||
return llm_text_gen(prompt, system_prompt)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating content: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def ai_story(persona, story_setting, character_input,
|
||||
plot_elements, writing_style, story_tone, narrative_pov,
|
||||
audience_age_group, content_rating, ending_preference):
|
||||
"""
|
||||
Write a story using prompt chaining and iterative generation.
|
||||
|
||||
Parameters:
|
||||
persona (str): The persona statement for the author.
|
||||
story_setting (str): The setting of the story.
|
||||
character_input (str): The characters in the story.
|
||||
plot_elements (str): The plot elements of the story.
|
||||
writing_style (str): The writing style of the story.
|
||||
story_tone (str): The tone of the story.
|
||||
narrative_pov (str): The narrative point of view.
|
||||
audience_age_group (str): The target audience age group.
|
||||
content_rating (str): The content rating of the story.
|
||||
ending_preference (str): The preferred ending of the story.
|
||||
"""
|
||||
st.info(f"""
|
||||
You have chosen to create a story set in **{story_setting}**.
|
||||
The main characters are: **{character_input}**.
|
||||
The plot will revolve around the theme of **{plot_elements}**.
|
||||
The story will be written in a **{writing_style}** style with a **{story_tone}** tone, from a **{narrative_pov}** perspective.
|
||||
It is intended for a **{audience_age_group}** audience with a **{content_rating}** rating.
|
||||
You prefer the story to have a **{ending_preference}** ending.
|
||||
""")
|
||||
try:
|
||||
persona = f"""{persona}
|
||||
Write a story with the following details:
|
||||
|
||||
**The stroy Setting is:**
|
||||
{story_setting}
|
||||
|
||||
**The Characters of the story are:**
|
||||
{character_input}
|
||||
|
||||
**Plot Elements of the story:**
|
||||
{plot_elements}
|
||||
|
||||
**Story Writing Style:**
|
||||
{writing_style}
|
||||
|
||||
**The story Tone is:**
|
||||
{story_tone}
|
||||
|
||||
**Write story from the Point of View of:**
|
||||
{narrative_pov}
|
||||
|
||||
**Target Audience of the story:**
|
||||
{audience_age_group}, **Content Rating:** {content_rating}
|
||||
|
||||
**Story Ending:**
|
||||
{ending_preference}
|
||||
|
||||
Make sure the story is engaging and tailored to the specified audience and content rating.
|
||||
Ensure the ending aligns with the preference indicated.
|
||||
|
||||
"""
|
||||
# Define persona and writing guidelines
|
||||
guidelines = f'''\
|
||||
Writing Guidelines:
|
||||
|
||||
Delve deeper. Lose yourself in the world you're building. Unleash vivid
|
||||
descriptions to paint the scenes in your reader's mind.
|
||||
Develop your characters — let their motivations, fears, and complexities unfold naturally.
|
||||
Weave in the threads of your outline, but don't feel constrained by it.
|
||||
Allow your story to surprise you as you write. Use rich imagery, sensory details, and
|
||||
evocative language to bring the setting, characters, and events to life.
|
||||
Introduce elements subtly that can blossom into complex subplots, relationships,
|
||||
or worldbuilding details later in the story.
|
||||
Keep things intriguing but not fully resolved.
|
||||
Avoid boxing the story into a corner too early.
|
||||
Plant the seeds of subplots or potential character arc shifts that can be expanded later.
|
||||
|
||||
Remember, your main goal is to write as much as you can. If you get through
|
||||
the story too fast, that is bad. Expand, never summarize.
|
||||
'''
|
||||
|
||||
# Generate prompts
|
||||
premise_prompt = f'''\
|
||||
{persona}
|
||||
|
||||
Write a single sentence premise for a {story_setting} story featuring {character_input}.
|
||||
'''
|
||||
|
||||
outline_prompt = f'''\
|
||||
{persona}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{{premise}}
|
||||
|
||||
Write an outline for the plot of your story.
|
||||
'''
|
||||
|
||||
starting_prompt = f'''\
|
||||
{persona}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{{premise}}
|
||||
|
||||
Your imagination has crafted a rich narrative outline:
|
||||
|
||||
{{outline}}
|
||||
|
||||
First, silently review the outline and the premise. Consider how to start the
|
||||
story.
|
||||
|
||||
Start to write the very beginning of the story. You are not expected to finish
|
||||
the whole story now. Your writing should be detailed enough that you are only
|
||||
scratching the surface of the first bullet of your outline. Try to write AT
|
||||
MINIMUM 4000 WORDS.
|
||||
|
||||
{guidelines}
|
||||
'''
|
||||
|
||||
continuation_prompt = f'''\
|
||||
{persona}
|
||||
|
||||
You have a gripping premise in mind:
|
||||
|
||||
{{premise}}
|
||||
|
||||
Your imagination has crafted a rich narrative outline:
|
||||
|
||||
{{outline}}
|
||||
|
||||
You've begun to immerse yourself in this world, and the words are flowing.
|
||||
Here's what you've written so far:
|
||||
|
||||
{{story_text}}
|
||||
|
||||
=====
|
||||
|
||||
First, silently review the outline and story so far. Identify what the single
|
||||
next part of your outline you should write.
|
||||
|
||||
Your task is to continue where you left off and write the next part of the story.
|
||||
You are not expected to finish the whole story now. Your writing should be
|
||||
detailed enough that you are only scratching the surface of the next part of
|
||||
your outline. Try to write AT MINIMUM 2000 WORDS. However, only once the story
|
||||
is COMPLETELY finished, write IAMDONE. Remember, do NOT write a whole chapter
|
||||
right now.
|
||||
|
||||
{guidelines}
|
||||
'''
|
||||
|
||||
# Generate prompts
|
||||
try:
|
||||
premise = generate_with_retry(premise_prompt)
|
||||
st.info(f"The premise of the story is: {premise}")
|
||||
except Exception as err:
|
||||
st.error(f"Premise Generation Error: {err}")
|
||||
return
|
||||
|
||||
outline = generate_with_retry(outline_prompt.format(premise=premise))
|
||||
with st.expander("Click to Checkout the outline, writing still in progress.."):
|
||||
st.markdown(f"The Outline of the story is: {outline}\n\n")
|
||||
|
||||
if not outline:
|
||||
st.error("Failed to generate outline. Exiting...")
|
||||
return
|
||||
|
||||
# Generate starting draft
|
||||
try:
|
||||
starting_draft = generate_with_retry(
|
||||
starting_prompt.format(premise=premise, outline=outline))
|
||||
except Exception as err:
|
||||
st.error(f"Failed to Generate Story draft: {err}")
|
||||
return
|
||||
|
||||
try:
|
||||
draft = starting_draft
|
||||
continuation = generate_with_retry(
|
||||
continuation_prompt.format(premise=premise, outline=outline, story_text=draft))
|
||||
except Exception as err:
|
||||
st.error(f"Failed to write the initial draft: {err}")
|
||||
|
||||
# Add the continuation to the initial draft, keep building the story until we see 'IAMDONE'
|
||||
try:
|
||||
draft += '\n\n' + continuation
|
||||
except Exception as err:
|
||||
st.error(f"Failed as: {err} and {continuation}")
|
||||
|
||||
with st.status("Story Writing in Progress..", expanded=True) as status:
|
||||
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
|
||||
while 'IAMDONE' not in continuation:
|
||||
try:
|
||||
status.update(label=f"Writing in progress... Current draft length: {len(draft)} characters")
|
||||
continuation = generate_with_retry(
|
||||
continuation_prompt.format(premise=premise, outline=outline, story_text=draft))
|
||||
draft += '\n\n' + continuation
|
||||
except Exception as err:
|
||||
st.error(f"Failed to continually write the story: {err}")
|
||||
return
|
||||
|
||||
# Remove 'IAMDONE' and print the final story
|
||||
final = draft.replace('IAMDONE', '').strip()
|
||||
return(final)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Main Story writing: An error occurred: {e}")
|
||||
return ""
|
||||
@@ -1,134 +0,0 @@
|
||||
import time
|
||||
import os
|
||||
import json
|
||||
import streamlit as st
|
||||
|
||||
from .ai_story_generator import ai_story
|
||||
|
||||
|
||||
def story_input_section():
|
||||
st.title("🧕 Alwrity - AI Story Writer")
|
||||
personas = [
|
||||
("Award-Winning Science Fiction Author", "👽 Award-Winning Science Fiction Author"),
|
||||
("Historical Fiction Author", "🏺 Historical Fiction Author"),
|
||||
("Fantasy World Builder", "🧙 Fantasy World Builder"),
|
||||
("Mystery Novelist", "🕵️ Mystery Novelist"),
|
||||
("Romantic Poet", "💌 Romantic Poet"),
|
||||
("Thriller Writer", "🔪 Thriller Writer"),
|
||||
("Children's Book Author", "📚 Children's Book Author"),
|
||||
("Satirical Humorist", "😂 Satirical Humorist"),
|
||||
("Biographical Writer", "📜 Biographical Writer"),
|
||||
("Dystopian Visionary", "🌆 Dystopian Visionary"),
|
||||
("Magical Realism Author", "🪄 Magical Realism Author")
|
||||
]
|
||||
|
||||
selected_persona_name = st.selectbox(
|
||||
"Select Your Story Writing Persona Or Book Genre",
|
||||
options=[persona[0] for persona in personas]
|
||||
)
|
||||
|
||||
persona_descriptions = {
|
||||
"Award-Winning Science Fiction Author": "You are an award-winning science fiction author with a penchant for expansive, intricately woven stories. Your ultimate goal is to write the next award-winning sci-fi novel.",
|
||||
"Historical Fiction Author": "You are a seasoned historical fiction author, meticulously researching past eras to weave captivating narratives. Your goal is to transport readers to different times and places through your vivid storytelling.",
|
||||
"Fantasy World Builder": "You are a world-building enthusiast, crafting intricate realms filled with magic, mythical creatures, and epic quests. Your ambition is to create the next immersive fantasy saga that captivates readers' imaginations.",
|
||||
"Mystery Novelist": "You are a master of suspense and intrigue, intricately plotting out mysteries with unexpected twists and turns. Your aim is to keep readers on the edge of their seats, eagerly turning pages to unravel the truth.",
|
||||
"Romantic Poet": "You are a romantic at heart, composing verses that capture the essence of love, longing, and human connections. Your dream is to write the next timeless love story that leaves readers swooning.",
|
||||
"Thriller Writer": "You are a thrill-seeker, crafting adrenaline-pumping tales of danger, suspense, and high-stakes action. Your mission is to keep readers hooked from start to finish with heart-pounding thrills and unexpected twists.",
|
||||
"Children's Book Author": "You are a storyteller for the young and young at heart, creating whimsical worlds and lovable characters that inspire imagination and wonder. Your goal is to spark joy and curiosity in young readers with enchanting tales.",
|
||||
"Satirical Humorist": "You are a keen observer of society, using humor and wit to satirize the absurdities of everyday life. Your aim is to entertain and provoke thought, delivering biting social commentary through clever and humorous storytelling.",
|
||||
"Biographical Writer": "You are a chronicler of lives, delving into the stories of real people and events to illuminate the human experience. Your passion is to bring history to life through richly detailed biographies that resonate with readers.",
|
||||
"Dystopian Visionary": "You are a visionary writer, exploring dark and dystopian futures that reflect contemporary fears and anxieties. Your vision is to challenge societal norms and provoke reflection on the path humanity is heading.",
|
||||
"Magical Realism Author": "You are a purveyor of magical realism, blending the ordinary with the extraordinary to create enchanting and thought-provoking tales. Your goal is to blur the lines between reality and fantasy, leaving readers enchanted and introspective."
|
||||
}
|
||||
|
||||
# Story Setting
|
||||
st.subheader("🌍 Story Setting")
|
||||
story_setting = st.text_area(
|
||||
label="**Story Setting** (e.g., medieval kingdom in the past, futuristic city in the future, haunted house in the present):",
|
||||
placeholder="""Enter settings for your story, like Location (e.g., medieval kingdom, futuristic city, haunted house),
|
||||
Time period in which your story is set (e.g: Past, Present, Future)
|
||||
Example: 'A bustling futuristic city with towering skyscrapers and flying cars, set in the year 2150.
|
||||
The city is known for its technological advancements but has a dark underbelly of crime and corruption.'""",
|
||||
help="Describe the main location and time period where the story will unfold in a detailed manner."
|
||||
)
|
||||
|
||||
# Main Characters
|
||||
st.subheader("👥 Main Characters")
|
||||
character_input = st.text_area(
|
||||
label="**Character Information** (Names, Descriptions, Roles)",
|
||||
placeholder="""Example:
|
||||
Character Names: John, Xishan, Amol
|
||||
Character Descriptions: John is a tall, muscular man with a kind heart. Xishan is a clever and resourceful woman. Amol is a mischievous and energetic young boy.
|
||||
Character Roles: John - Hero, Xishan - Sidekick, Amol - Supporting Character""",
|
||||
help="Enter character information as specified in the placeholder."
|
||||
)
|
||||
|
||||
# Plot Elements
|
||||
st.subheader("🗺️ Plot Elements")
|
||||
plot_elements = st.text_area(
|
||||
"**Plot Elements** - (Theme, Key Events & Main Conflict)",
|
||||
placeholder="""Example:
|
||||
Story Theme: Love conquers all, The hero's journey, Good vs. evil.
|
||||
Key Events: The hero meets the villain, The hero faces a challenge, The hero overcomes the conflict.
|
||||
Main Conflict: The hero must save the world from a powerful enemy, The hero must overcome a personal obstacle to achieve their goal.""",
|
||||
help="Enter plot elements as specified in the placeholder."
|
||||
)
|
||||
|
||||
# Tone and Style
|
||||
st.subheader("🎨 Tone and Style")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
writing_style = st.selectbox(
|
||||
"**Writing Style:**",
|
||||
["🧐 Formal", "😎 Casual", "🎼 Poetic", "😂 Humorous"],
|
||||
help="Choose the writing style that fits your story."
|
||||
)
|
||||
with col2:
|
||||
story_tone = st.selectbox(
|
||||
"**Story Tone:**",
|
||||
["🌑 Dark", "☀️ Uplifting", "⏳ Suspenseful", "🎈 Whimsical"],
|
||||
help="Select the overall tone or mood of the story."
|
||||
)
|
||||
with col3:
|
||||
narrative_pov = st.selectbox(
|
||||
"**Narrative Point of View:**",
|
||||
["👤 First Person", "👥 Third Person Limited", "👁️ Third Person Omniscient"],
|
||||
help="Choose the point of view from which the story is told."
|
||||
)
|
||||
|
||||
# Target Audience
|
||||
st.subheader("👨👩👧👦 Target Audience")
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
audience_age_group = st.selectbox(
|
||||
"**Audience Age Group:**",
|
||||
["🧒 Children", "👨🎓 Young Adults", "🧑🦳 Adults"],
|
||||
help="Choose the intended audience age group."
|
||||
)
|
||||
with col2:
|
||||
content_rating = st.selectbox(
|
||||
"**Content Rating:**",
|
||||
["🟢 G", "🟡 PG", "🔵 PG-13", "🔴 R"],
|
||||
help="Select a content rating for appropriateness."
|
||||
)
|
||||
with col3:
|
||||
ending_preference = st.selectbox(
|
||||
"Story Conclusion:",
|
||||
["😊 Happy", "😢 Tragic", "❓ Cliffhanger", "🔀 Twist"],
|
||||
help="Choose the type of ending you prefer for the story."
|
||||
)
|
||||
|
||||
if st.button('AI, Write a Story..'):
|
||||
if character_input.strip():
|
||||
with st.spinner("Generating Story...💥💥"):
|
||||
story_content = ai_story(persona_descriptions[selected_persona_name],
|
||||
story_setting, character_input, plot_elements, writing_style,
|
||||
story_tone, narrative_pov, audience_age_group, content_rating,
|
||||
ending_preference)
|
||||
if story_content:
|
||||
st.subheader('**🧕 Your Awesome Story:**')
|
||||
st.markdown(story_content)
|
||||
else:
|
||||
st.error("💥 **Failed to generate Story. Please try again!**")
|
||||
else:
|
||||
st.error("Describe the story you have in your mind.. !")
|
||||
@@ -1,220 +0,0 @@
|
||||
import streamlit as st
|
||||
from lib.utils.alwrity_utils import (essay_writer, ai_news_writer, ai_finance_ta_writer)
|
||||
|
||||
from lib.ai_writers.ai_story_writer.story_writer import story_input_section
|
||||
from lib.ai_writers.ai_product_description_writer import write_ai_prod_desc
|
||||
from lib.ai_writers.ai_copywriter.copywriter_dashboard import copywriter_dashboard
|
||||
from lib.ai_writers.linkedin_writer import LinkedInAIWriter
|
||||
from lib.ai_writers.blog_rewriter_updater.ai_blog_rewriter import write_blog_rewriter
|
||||
from lib.ai_writers.ai_blog_faqs_writer.faqs_ui import main as faqs_generator
|
||||
from lib.ai_writers.ai_blog_writer.ai_blog_generator import ai_blog_writer_page
|
||||
from lib.ai_writers.ai_outline_writer.outline_ui import main as outline_generator
|
||||
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)."""
|
||||
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": "📝",
|
||||
"description": "Generate comprehensive blog posts from keywords, URLs, or uploaded content",
|
||||
"category": "Content Creation",
|
||||
"function": ai_blog_writer_page,
|
||||
"path": "ai_blog_writer"
|
||||
},
|
||||
{
|
||||
"name": "AI Blog Rewriter",
|
||||
"icon": "🔄",
|
||||
"description": "Rewrite and update existing blog content with improved quality and SEO optimization",
|
||||
"category": "Content Creation",
|
||||
"function": write_blog_rewriter,
|
||||
"path": "blog_rewriter"
|
||||
},
|
||||
{
|
||||
"name": "Story Writer",
|
||||
"icon": "📚",
|
||||
"description": "Create engaging stories and narratives with AI assistance",
|
||||
"category": "Creative Writing",
|
||||
"function": story_input_section,
|
||||
"path": "story_writer"
|
||||
},
|
||||
{
|
||||
"name": "Essay writer",
|
||||
"icon": "✍️",
|
||||
"description": "Generate well-structured essays on any topic",
|
||||
"category": "Academic",
|
||||
"function": essay_writer,
|
||||
"path": "essay_writer"
|
||||
},
|
||||
{
|
||||
"name": "Write News reports",
|
||||
"icon": "📰",
|
||||
"description": "Create professional news articles and reports",
|
||||
"category": "Journalism",
|
||||
"function": ai_news_writer,
|
||||
"path": "news_writer"
|
||||
},
|
||||
{
|
||||
"name": "Write Financial TA report",
|
||||
"icon": "📊",
|
||||
"description": "Generate technical analysis reports for financial markets",
|
||||
"category": "Finance",
|
||||
"function": ai_finance_ta_writer,
|
||||
"path": "financial_writer"
|
||||
},
|
||||
{
|
||||
"name": "AI Product Description Writer",
|
||||
"icon": "🛍️",
|
||||
"description": "Create compelling product descriptions that drive sales",
|
||||
"category": "E-commerce",
|
||||
"function": write_ai_prod_desc,
|
||||
"path": "product_writer"
|
||||
},
|
||||
{
|
||||
"name": "AI Copywriter",
|
||||
"icon": "✒️",
|
||||
"description": "Generate persuasive copy for marketing and advertising",
|
||||
"category": "Marketing",
|
||||
"function": copywriter_dashboard,
|
||||
"path": "copywriter"
|
||||
},
|
||||
{
|
||||
"name": "LinkedIn AI Writer",
|
||||
"icon": "💼",
|
||||
"description": "Create professional LinkedIn content that engages your network",
|
||||
"category": "Professional",
|
||||
"function": lambda: LinkedInAIWriter().run(),
|
||||
"path": "linkedin_writer"
|
||||
},
|
||||
{
|
||||
"name": "FAQ Generator",
|
||||
"icon": "❓",
|
||||
"description": "Generate comprehensive, well-researched FAQs from any content source with customizable options",
|
||||
"category": "Content Creation",
|
||||
"function": faqs_generator,
|
||||
"path": "faqs_generator"
|
||||
},
|
||||
{
|
||||
"name": "Blog Outline Generator",
|
||||
"icon": "📋",
|
||||
"description": "Create detailed blog outlines with AI-powered content generation and image integration",
|
||||
"category": "Content Creation",
|
||||
"function": outline_generator,
|
||||
"path": "outline_generator"
|
||||
}
|
||||
])
|
||||
|
||||
return writers
|
||||
|
||||
def get_ai_writers():
|
||||
"""Main function to display AI writers dashboard with premium glassmorphic design."""
|
||||
logger.info("Starting AI Writers Dashboard")
|
||||
|
||||
# Apply common dashboard styling
|
||||
apply_dashboard_style()
|
||||
|
||||
# Render dashboard header
|
||||
render_dashboard_header(
|
||||
"🤖 AI Content Writers",
|
||||
"Choose from our collection of specialized AI writers, each designed for specific content types and industries. Create engaging, high-quality content with just a few clicks."
|
||||
)
|
||||
|
||||
writers = list_ai_writers()
|
||||
logger.info(f"Found {len(writers)} AI writers")
|
||||
|
||||
# Group writers by category for better organization
|
||||
categories = {}
|
||||
for writer in writers:
|
||||
category = writer["category"]
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(writer)
|
||||
|
||||
# Render writers by category with common cards
|
||||
for category_name, category_writers in categories.items():
|
||||
render_category_header(category_name)
|
||||
|
||||
# Create columns for this category
|
||||
cols = st.columns(min(len(category_writers), 3))
|
||||
|
||||
for idx, writer in enumerate(category_writers):
|
||||
with cols[idx % 3]:
|
||||
# Use the common card renderer
|
||||
if render_card(
|
||||
icon=writer['icon'],
|
||||
title=writer['name'],
|
||||
description=writer['description'],
|
||||
category=writer['category'],
|
||||
key_suffix=f"{writer['path']}_{category_name}",
|
||||
help_text=f"Launch {writer['name']} - {writer['description']}"
|
||||
):
|
||||
logger.info(f"Selected writer: {writer['name']} with path: {writer['path']}")
|
||||
st.session_state.selected_writer = writer
|
||||
st.query_params["writer"] = writer['path']
|
||||
logger.info(f"Updated query params with writer: {writer['path']}")
|
||||
st.rerun()
|
||||
|
||||
# Add spacing between categories
|
||||
st.markdown('<div class="category-spacer"></div>', unsafe_allow_html=True)
|
||||
|
||||
logger.info("Finished rendering AI Writers Dashboard")
|
||||
|
||||
return writers
|
||||
|
||||
# Remove the old ai_writers function since it's now integrated into get_ai_writers
|
||||
@@ -1,50 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
from ..gpt_providers.text_generation.openai_text_gen import openai_text_generation
|
||||
from ..gpt_providers.text_generation.gemini_pro_text import gemini_text_generation
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
|
||||
# FIXME: Provide num_blogs, num_faqs as inputs.
|
||||
def get_blog_sections_from_websearch(search_keyword, search_results):
|
||||
"""Combine the given online research and gpt blog content"""
|
||||
gpt_providers = os.environ["GPT_PROVIDER"]
|
||||
prompt = f"""
|
||||
As a SEO expert and content writer, I will provide you with a search keyword and its google search result.
|
||||
Your task is to write a blog title and 5 blog sub titles, from the given google search result.
|
||||
The subtitles should be less than 40 characters and click worthy.
|
||||
Do not explain, describe your response. Respond in json format, always name the key as 'blogSections'.
|
||||
|
||||
Web Research Keyword: "{search_keyword}"
|
||||
Google search Result: "{search_results}"
|
||||
"""
|
||||
|
||||
if 'gemini' in gpt_providers:
|
||||
try:
|
||||
response = gemini_text_response(prompt)
|
||||
if '```' in response and '\n' in response:
|
||||
response = response.strip().split('\n')
|
||||
# Remove the first and last lines
|
||||
response = '\n'.join(response[1:-1])
|
||||
response = json.loads(response)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from gemini: {err}")
|
||||
logger.error(f"Gemini Error: {response.prompt_feedback}")
|
||||
raise err
|
||||
elif 'openai' in gpt_providers:
|
||||
try:
|
||||
logger.info("Calling OpenAI LLM.")
|
||||
response = openai_chatgpt(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to get response from Openai: {err}")
|
||||
raise err
|
||||
@@ -1,109 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from textwrap import dedent
|
||||
import json
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import streamlit as st
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path('../../.env'))
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ..ai_web_researcher.firecrawl_web_crawler import scrape_url
|
||||
from ..blog_metadata.get_blog_metadata import blog_metadata
|
||||
from ..blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from ..gpt_providers.image_to_text_gen.gemini_image_describe import describe_image, analyze_image_with_prompt
|
||||
|
||||
|
||||
def blog_from_image(prompt, uploaded_img):
|
||||
"""
|
||||
This function will take a blog Topic to first generate sections for it
|
||||
and then generate content for each section.
|
||||
"""
|
||||
# Use to store the blog in a string, to save in a *.md file.
|
||||
blog_markdown_str = None
|
||||
logger.info(f"Researching and Writing Blog on {uploaded_img} and {prompt}")
|
||||
# FIXME: Implement support for Openai.
|
||||
if not os.getenv("GEMINI_API_KEY"):
|
||||
st.error("Only Gemini supported, Open Issue ticket on github for Openai, others.")
|
||||
st.stop()
|
||||
|
||||
with st.status("Started Writing from Image..", expanded=True) as status:
|
||||
st.empty()
|
||||
status.update(label=f"Researching and Writing Blog on given Image")
|
||||
try:
|
||||
blog_markdown_str = write_blog_from_image(prompt, uploaded_img)
|
||||
except Exception as err:
|
||||
st.error(f"Failed to write blog from Image - Error: {err}")
|
||||
logger.error(f"Failed to write blog from image: {err}")
|
||||
st.stop()
|
||||
status.update(label="Successfully wrote blog from image.", expanded=False, state="complete")
|
||||
|
||||
try:
|
||||
status.update(label="🙎 Generating - Title, Meta Description, Tags, Categories for the content.")
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories = asyncio.run(blog_metadata(blog_markdown_str))
|
||||
except Exception as err:
|
||||
st.error(f"Failed to get blog metadata: {err}")
|
||||
|
||||
try:
|
||||
status.update(label="🙎 Generating Image for the new blog.")
|
||||
generated_image_filepath = generate_image(f"{blog_title} + ' ' + {blog_meta_desc}")
|
||||
except Exception as err:
|
||||
st.warning(f"Failed in Image generation: {err}")
|
||||
|
||||
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
|
||||
blog_tags, blog_categories, generated_image_filepath)
|
||||
status.update(label=f"Saved the content in this file: {saved_blog_to_file}")
|
||||
logger.info(f"\n\n --------- Finished writing Blog -------------- \n")
|
||||
st.image(generated_image_filepath, caption=blog_title)
|
||||
st.markdown(f"{blog_markdown_str}")
|
||||
status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}", state="complete")
|
||||
|
||||
# Clean up the temporary file after processing (optional)
|
||||
os.remove(uploaded_img)
|
||||
|
||||
|
||||
def write_blog_from_image(prompt, uploaded_img):
|
||||
"""Combine the given online research and GPT blog content"""
|
||||
try:
|
||||
config_path = Path(os.environ["ALWRITY_CONFIG"])
|
||||
with open(config_path, 'r', encoding='utf-8') as file:
|
||||
config = json.load(file)
|
||||
except Exception as err:
|
||||
logger.error(f"Error: Failed to read values from config: {err}")
|
||||
exit(1)
|
||||
|
||||
blog_characteristics = config['Blog Content Characteristics']
|
||||
|
||||
if not prompt:
|
||||
prompt = f"""
|
||||
As expert Creative Content writer, analyse the given image carefully.
|
||||
I want you to write a detailed {blog_characteristics['Blog Type']} blog post including 5 FAQs.
|
||||
|
||||
Below are the guidelines to follow:
|
||||
1). You must respond in {blog_characteristics['Blog Language']} language.
|
||||
2). Tone and Brand Alignment: Adjust your tone, voice, personality for {blog_characteristics['Blog Tone']} audience.
|
||||
3). Make sure your response content length is of {blog_characteristics['Blog Length']} words.
|
||||
"""
|
||||
logger.info("Generating blog and FAQs from image analysis.")
|
||||
|
||||
try:
|
||||
# Use the gemini_image_describe function to analyze the image with the custom prompt
|
||||
response = analyze_image_with_prompt(uploaded_img, prompt)
|
||||
if not response:
|
||||
logger.error("Failed to get response from image analysis")
|
||||
return "Failed to generate content from image."
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Exit: Failed to get response from image analysis: {err}")
|
||||
exit(1)
|
||||
@@ -1,143 +0,0 @@
|
||||
import os
|
||||
import datetime #I wish
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from tqdm import tqdm, trange
|
||||
import time
|
||||
|
||||
from pytubefix import YouTube
|
||||
import tempfile
|
||||
from html2image import Html2Image
|
||||
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ...ai_web_researcher.gpt_online_researcher import do_google_serp_search
|
||||
from ..ai_blog_writer.blog_from_google_serp import blog_with_research
|
||||
from ...blog_metadata.get_blog_metadata import blog_metadata
|
||||
from ...blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
from ...gpt_providers.audio_to_text_generation.stt_audio_blog import speech_to_text
|
||||
from ...gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def youtube_to_blog(video_url):
|
||||
"""Function to transcribe a given youtube url """
|
||||
try:
|
||||
# Starting the speech-to-text process
|
||||
logger.info("Starting with Speech to Text.")
|
||||
audio_text, audio_title = speech_to_text(video_url)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in speech_to_text: {e}")
|
||||
sys.exit(1) # Exit the program due to error in speech_to_text
|
||||
|
||||
try:
|
||||
# Summarizing the content of the YouTube video
|
||||
audio_blog_content = summarize_youtube_video(audio_text)
|
||||
logger.info("Successfully converted given URL to blog article.")
|
||||
return audio_blog_content, audio_title
|
||||
except Exception as e:
|
||||
logger.error(f"Error in summarize_youtube_video: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def summarize_youtube_video(user_content):
|
||||
"""Generates a summary of a YouTube video using OpenAI GPT-3 and displays a progress bar.
|
||||
Args:
|
||||
video_link: The URL of the YouTube video to summarize.
|
||||
Returns:
|
||||
A string containing the summary of the video.
|
||||
"""
|
||||
|
||||
logger.info("Start summarize_youtube_video..")
|
||||
prompt = f"""
|
||||
You are an expert copywriter specializing in digital content writing. I will provide you with a transcript.
|
||||
Your task is to transform a given transcript into a well-structured and informative blog article.
|
||||
Please follow the below objectives:
|
||||
|
||||
1. Master the Transcript: Understand main ideas, key points, and the core message.
|
||||
2. Sentence Structure: Rephrase while preserving logical flow and coherence. Dont quote anyone from video.
|
||||
3. Note: Check if the transcript is about programming, then include code examples and snippets in your article.
|
||||
4. Write Unique Content: Avoid direct copying; rewrite in your own words.
|
||||
5. REMEMBER to avoid direct quoting and maintain uniqueness.
|
||||
6. Proofread: Check for grammar, spelling, and punctuation errors.
|
||||
7. Use Creative and Human-like Style: Incorporate contractions, idioms, transitional phrases, interjections, and colloquialisms. 8. Avoid repetitive phrases and unnatural sentence structures.
|
||||
9. Ensure Uniqueness: Guarantee the article is plagiarism-free.
|
||||
10. Punctuation: Use appropriate question marks at the end of questions.
|
||||
11. Pass AI Detection Tools: Create content that easily passes AI plagiarism detection tools.
|
||||
12. Rephrase words like 'video, youtube, channel' with 'article, blog' and such suitable words.
|
||||
|
||||
Follow the above guidelines to create a well-optimized, unique, and informative article,
|
||||
that will rank well in search engine results and engage readers effectively.
|
||||
Follow above guidelines to craft a blog content from the following transcript:\n{user_content}
|
||||
"""
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to summarize_youtube_video: {err}")
|
||||
exit(1)
|
||||
|
||||
|
||||
def generate_audio_blog(audio_input):
|
||||
"""Takes a list of youtube videos and generates blog for each one of them.
|
||||
"""
|
||||
# Use to store the blog in a string, to save in a *.md file.
|
||||
blog_markdown_str = ""
|
||||
try:
|
||||
logger.info(f"Starting to write blog on URL: {audio_input}")
|
||||
yt_blog, yt_title = youtube_to_blog(audio_input)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in youtube_to_blog: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
logger.info("Starting with online research for URL title.")
|
||||
research_report = do_google_serp_search(yt_title)
|
||||
print(research_report)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in do_online_research: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
# Note: Check if the order of input matters for your function
|
||||
logger.info("Preparing a blog content from audio script and online research content...")
|
||||
blog_markdown_str = blog_with_research(research_report, yt_blog)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in blog_with_research: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
# blog_metadata now returns 6 values: title, desc, tags, categories, hashtags, slug
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories, blog_hashtags, blog_slug = asyncio.run(blog_metadata(blog_markdown_str))
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to generate blog metadata: {err}")
|
||||
# Set defaults in case of failure
|
||||
blog_title = "Blog Article"
|
||||
blog_meta_desc = "An informative blog post"
|
||||
blog_tags = "content, blog"
|
||||
blog_categories = "General, Information"
|
||||
blog_hashtags = "#content #blog"
|
||||
blog_slug = "blog-article"
|
||||
|
||||
try:
|
||||
# TBD: Save the blog content as a .md file. Markdown or HTML ?
|
||||
# Initialize generated_image_filepath to None since it's not generated in this function
|
||||
generated_image_filepath = None
|
||||
save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc, blog_tags, blog_categories, generated_image_filepath)
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to save final blog in a file: {err}")
|
||||
|
||||
blog_frontmatter = dedent(f"""\n\n\n\
|
||||
---
|
||||
title: {blog_title}
|
||||
categories: [{blog_categories}]
|
||||
tags: [{blog_tags}]
|
||||
Meta description: {blog_meta_desc.replace(":", "-")}
|
||||
---\n\n""")
|
||||
logger.info(f"{blog_frontmatter}{blog_markdown_str}")
|
||||
logger.info(f"\n\n ################ Finished writing Blog for : {audio_input} #################### \n")
|
||||
@@ -1,165 +0,0 @@
|
||||
# Twitter AI Writer Module
|
||||
|
||||
A comprehensive suite of AI-powered tools for Twitter/X content marketing and management.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Tweet Generation & Optimization
|
||||
- **Smart Tweet Generator**
|
||||
- Multiple tweet variations based on input parameters
|
||||
- Character count optimization
|
||||
- Hashtag suggestions and placement
|
||||
- Emoji usage recommendations
|
||||
- Thread creation capabilities
|
||||
|
||||
- **Tweet Performance Predictor**
|
||||
- Engagement rate estimation
|
||||
- Best time to post suggestions
|
||||
- Audience reach predictions
|
||||
- Viral potential scoring
|
||||
|
||||
### 2. Content Strategy Tools
|
||||
- **Content Calendar Generator**
|
||||
- Weekly/monthly content planning
|
||||
- Theme-based content scheduling
|
||||
- Event and holiday integration
|
||||
- Content mix recommendations
|
||||
|
||||
- **Hashtag Strategy Manager**
|
||||
- Trending hashtag research
|
||||
- Custom hashtag creation
|
||||
- Hashtag performance tracking
|
||||
- Competitor hashtag analysis
|
||||
|
||||
### 3. Visual Content Creation
|
||||
- **Image Generator**
|
||||
- Tweet card creation
|
||||
- Infographic templates
|
||||
- Quote card designs
|
||||
- Brand-consistent visuals
|
||||
|
||||
- **Video Content Assistant**
|
||||
- Video script generation
|
||||
- Storyboard creation
|
||||
- Caption optimization
|
||||
- Thumbnail design suggestions
|
||||
|
||||
### 4. Engagement & Community Management
|
||||
- **Reply Generator**
|
||||
- Context-aware responses
|
||||
- Tone matching
|
||||
- Crisis management templates
|
||||
- Customer service responses
|
||||
|
||||
- **Community Engagement Tools**
|
||||
- Poll creation
|
||||
- Q&A session planning
|
||||
- Community highlight suggestions
|
||||
- User-generated content prompts
|
||||
|
||||
### 5. Analytics & Optimization
|
||||
- **Performance Analytics**
|
||||
- Tweet performance tracking
|
||||
- Engagement metrics analysis
|
||||
- Audience growth monitoring
|
||||
- Content effectiveness scoring
|
||||
|
||||
- **A/B Testing Assistant**
|
||||
- Tweet variation testing
|
||||
- Headline optimization
|
||||
- CTA effectiveness analysis
|
||||
- Best performing content identification
|
||||
|
||||
### 6. Research & Intelligence
|
||||
- **Market Research Tools**
|
||||
- Competitor analysis
|
||||
- Industry trend tracking
|
||||
- Audience sentiment analysis
|
||||
- Content gap identification
|
||||
|
||||
- **Content Inspiration**
|
||||
- Trending topic suggestions
|
||||
- Content idea generation
|
||||
- Viral content analysis
|
||||
- Industry-specific insights
|
||||
|
||||
## Best Practices Integration
|
||||
|
||||
### Tweet Optimization
|
||||
- Optimal character count (240-280)
|
||||
- Strategic hashtag placement
|
||||
- Effective use of mentions and links
|
||||
- Engaging call-to-actions
|
||||
- Visual content optimization
|
||||
|
||||
### Content Strategy
|
||||
- Consistent brand voice
|
||||
- Regular posting schedule
|
||||
- Content variety maintenance
|
||||
- Engagement-driven approach
|
||||
- Community building focus
|
||||
|
||||
### Visual Content
|
||||
- Image size optimization
|
||||
- Brand color consistency
|
||||
- Text overlay best practices
|
||||
- Mobile-friendly design
|
||||
- Visual hierarchy principles
|
||||
|
||||
### Engagement
|
||||
- Response time optimization
|
||||
- Community management guidelines
|
||||
- Crisis communication protocols
|
||||
- User interaction best practices
|
||||
- Content moderation assistance
|
||||
|
||||
## Technical Integration
|
||||
|
||||
### API Integration
|
||||
- Twitter API v2 support
|
||||
- Rate limit management
|
||||
- Error handling
|
||||
- Data synchronization
|
||||
|
||||
### Performance Optimization
|
||||
- Caching mechanisms
|
||||
- Batch processing
|
||||
- Resource optimization
|
||||
- Response time improvement
|
||||
|
||||
## Security & Compliance
|
||||
|
||||
### Data Protection
|
||||
- User data encryption
|
||||
- Secure API key management
|
||||
- Privacy compliance
|
||||
- Data retention policies
|
||||
|
||||
### Content Guidelines
|
||||
- Platform policy compliance
|
||||
- Copyright protection
|
||||
- Brand safety measures
|
||||
- Content moderation rules
|
||||
|
||||
## Coming Soon
|
||||
- Advanced thread generator
|
||||
- AI-powered image editor
|
||||
- Real-time trend analyzer
|
||||
- Automated content scheduler
|
||||
- Advanced analytics dashboard
|
||||
- Multi-account management
|
||||
- Custom AI model training
|
||||
- Integration with other social platforms
|
||||
|
||||
## Usage Guidelines
|
||||
1. Ensure API keys are properly configured
|
||||
2. Follow Twitter's terms of service
|
||||
3. Maintain brand voice consistency
|
||||
4. Regular content calendar updates
|
||||
5. Monitor performance metrics
|
||||
6. Engage with community regularly
|
||||
7. Update content strategy based on analytics
|
||||
8. Follow security best practices
|
||||
|
||||
## Support
|
||||
For technical support or feature requests, please contact the development team or raise an issue in the repository. https://github.com/AJaySi/AI-Writer/issues
|
||||
@@ -1,9 +0,0 @@
|
||||
"""
|
||||
Twitter AI Writer Module
|
||||
|
||||
A comprehensive suite of AI-powered tools for Twitter/X content marketing and management.
|
||||
"""
|
||||
|
||||
from .twitter_dashboard import run_dashboard
|
||||
|
||||
__all__ = ['run_dashboard']
|
||||
@@ -1,163 +0,0 @@
|
||||
Here’s an improved and enhanced version of your README. I've structured it for clarity, conciseness, and professionalism, while also making it more engaging and user-friendly.
|
||||
|
||||
---
|
||||
|
||||
# 🐦 Smart Tweet Generator
|
||||
|
||||
**Create tweets that stand out!** The Smart Tweet Generator is a cutting-edge AI-powered tool designed to craft optimized, engaging tweets that maximize your audience reach and engagement.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
### 1. **Multi-Variation Tweet Generation**
|
||||
- Generate 1–5 tweet variations from a single prompt.
|
||||
- Each variation tailored to different engagement styles.
|
||||
- Consistent tone and messaging across all versions.
|
||||
|
||||
### 2. **Real-Time Character Optimization**
|
||||
- Live character count tracking, including emoji support.
|
||||
- Visual indicators to maintain the ideal tweet length.
|
||||
- Alerts when nearing Twitter's 280-character limit.
|
||||
|
||||
### 3. **Intelligent Hashtag Management**
|
||||
- Auto-extract hashtags from generated tweets.
|
||||
- Topic-based, AI-suggested hashtags to enhance discoverability.
|
||||
- Recommendations for optimal hashtag count and placement.
|
||||
|
||||
### 4. **Emoji Suggestions That Fit**
|
||||
- Context-sensitive and tone-appropriate emoji suggestions.
|
||||
- Categories include:
|
||||
- **Humorous**: 😄 😂 😉
|
||||
- **Informative**: 📊 🔍 💡
|
||||
- **Inspirational**: ✨ 🌟 🔥
|
||||
- **Serious**: 🤔 📢 🔔
|
||||
- **Casual**: 👋 👍 🤗
|
||||
|
||||
### 5. **Performance Prediction**
|
||||
- Engagement score (0-100%) based on AI analysis.
|
||||
- Metrics analyzed include:
|
||||
- Character count optimization.
|
||||
- Hashtag effectiveness.
|
||||
- Emoji usage.
|
||||
- Audience relevance.
|
||||
- Categories:
|
||||
- **Excellent** (80–100%)
|
||||
- **Good** (60–79%)
|
||||
- **Fair** (40–59%)
|
||||
- **Needs Improvement** (0–39%)
|
||||
|
||||
### 6. **Actionable Improvement Suggestions**
|
||||
- Real-time feedback on tweet quality.
|
||||
- Tailored recommendations to boost performance.
|
||||
- Built-in best practices guidance for effective tweeting.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 How to Use
|
||||
|
||||
### Step 1: **Enter Basic Information**
|
||||
- Add your tweet topic or hook.
|
||||
- Define the target audience.
|
||||
- Choose the desired tone and tweet length.
|
||||
- Optionally, include a call-to-action (CTA).
|
||||
|
||||
### Step 2: **Customize Advanced Options**
|
||||
- Select the number of tweet variations (1–5).
|
||||
- Input keywords or hashtags.
|
||||
- Choose emoji preferences.
|
||||
- Add @mentions or placeholders for links.
|
||||
|
||||
### Step 3: **Generate and Refine**
|
||||
- Click **Generate Tweets** to create variations.
|
||||
- Review performance metrics and apply improvement suggestions.
|
||||
- Copy, save, or export your favorite version.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance Metrics
|
||||
|
||||
**Your tweets are analyzed based on:**
|
||||
|
||||
1. **Character Count**
|
||||
- Optimal: 100–200 characters.
|
||||
- Short: <100 characters.
|
||||
- Long: >200 characters.
|
||||
|
||||
2. **Hashtag Usage**
|
||||
- Optimal: 1–3 hashtags.
|
||||
- Too few: 0 hashtags.
|
||||
- Too many: >3 hashtags.
|
||||
|
||||
3. **Engagement Triggers**
|
||||
- Questions, CTAs, or interactive elements.
|
||||
|
||||
4. **Emoji Optimization**
|
||||
- Ideal: 1–3 emojis.
|
||||
- Too few: 0 emojis.
|
||||
- Too many: >3 emojis.
|
||||
|
||||
5. **Audience Relevance**
|
||||
- Alignment with keywords, tone, and context.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Best Practices
|
||||
|
||||
1. **Craft Attention-Grabbing Hooks**
|
||||
- Start with bold statements or thought-provoking questions.
|
||||
- Use stats or facts to capture attention.
|
||||
|
||||
2. **Align Tone with Audience**
|
||||
- Maintain consistency with your brand voice.
|
||||
- Adapt tone to audience preferences (e.g., formal, casual).
|
||||
|
||||
3. **Strategic Hashtag Usage**
|
||||
- Use trending and relevant hashtags.
|
||||
- Limit to 1–3 for optimal engagement.
|
||||
|
||||
4. **Effective Emoji Usage**
|
||||
- Enhance meaning and context with emojis.
|
||||
- Match the tone and avoid overuse.
|
||||
|
||||
5. **Clear Calls-to-Action**
|
||||
- Encourage action with clarity and urgency.
|
||||
- Use action verbs like "Discover," "Join," or "Explore."
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Export Options
|
||||
|
||||
- Copy individual tweets.
|
||||
- Export all variations as a JSON file.
|
||||
- Save performance metrics and recommendations.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technical Details
|
||||
|
||||
- **Built with:** Streamlit for an intuitive user interface.
|
||||
- **AI-powered:** Advanced natural language models for tweet generation.
|
||||
- **Real-time:** Instant feedback and suggestions.
|
||||
- **Cross-platform compatibility:** Works seamlessly across devices.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Tweets are optimized for Twitter’s 280-character limit.
|
||||
- Performance predictions are derived from AI insights and engagement patterns.
|
||||
- Suggestions adapt to your audience, ensuring relevancy.
|
||||
- Regular updates keep the tool current with Twitter trends.
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Support
|
||||
|
||||
Have questions or feature requests? Reach out to our support team or submit an issue on our GitHub repository.
|
||||
|
||||
---
|
||||
|
||||
*Last updated: Yesterday*
|
||||
|
||||
---
|
||||
@@ -1,9 +0,0 @@
|
||||
"""
|
||||
Twitter Tweet Generator Module
|
||||
|
||||
A comprehensive suite of tools for generating and optimizing tweets.
|
||||
"""
|
||||
|
||||
from .smart_tweet_generator import smart_tweet_generator
|
||||
|
||||
__all__ = ['smart_tweet_generator']
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,729 +0,0 @@
|
||||
"""
|
||||
Enhanced Twitter Dashboard with modern UI components and improved user experience.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, List, Optional, Any
|
||||
import json
|
||||
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,
|
||||
Header,
|
||||
Tabs,
|
||||
Breadcrumbs,
|
||||
Theme,
|
||||
save_to_session,
|
||||
get_from_session,
|
||||
clear_session,
|
||||
show_success_message,
|
||||
show_error_message,
|
||||
show_info_message,
|
||||
show_warning_message
|
||||
)
|
||||
|
||||
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 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)
|
||||
|
||||
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_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:
|
||||
if st.button("✍️ Create Tweet", use_container_width=True, key="quick_tweet"):
|
||||
st.session_state.current_page = "tweet_generator"
|
||||
st.rerun()
|
||||
|
||||
with col2:
|
||||
if st.button("📊 View Analytics", use_container_width=True, key="quick_analytics"):
|
||||
st.session_state.current_page = "analytics"
|
||||
st.rerun()
|
||||
|
||||
with col3:
|
||||
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_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]
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
# 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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_dashboard()
|
||||
@@ -1,203 +0,0 @@
|
||||
# Twitter Streamlit UI Components
|
||||
|
||||
This module provides a unified, reusable UI component library for all Twitter-related features in the AI Writer suite. It implements best practices for Streamlit UI development and ensures consistency across all Twitter tools.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
twitter_streamlit_ui/
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── __init__.py
|
||||
│ ├── cards.py # Card components (feature cards, tweet cards)
|
||||
│ ├── forms.py # Form components (input forms, settings forms)
|
||||
│ ├── navigation.py # Navigation components (tabs, sidebar)
|
||||
│ ├── feedback.py # Feedback components (loading, errors, success)
|
||||
│ └── layout.py # Layout components (containers, columns)
|
||||
├── styles/ # CSS and styling
|
||||
│ ├── __init__.py
|
||||
│ ├── theme.py # Theme configuration
|
||||
│ ├── components.py # Component-specific styles
|
||||
│ └── animations.py # Animation styles
|
||||
├── utils/ # UI utilities
|
||||
│ ├── __init__.py
|
||||
│ ├── state.py # State management
|
||||
│ ├── validation.py # Input validation
|
||||
│ └── performance.py # Performance optimizations
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 1. Consistent UI Components
|
||||
|
||||
- **Card Components**
|
||||
- Feature cards with consistent styling
|
||||
- Tweet cards with standardized layout
|
||||
- Status badges with unified design
|
||||
|
||||
- **Form Components**
|
||||
- Standardized input forms
|
||||
- Consistent validation feedback
|
||||
- Unified error handling
|
||||
|
||||
- **Navigation Components**
|
||||
- Consistent tab styling
|
||||
- Standardized sidebar navigation
|
||||
- Breadcrumb navigation
|
||||
|
||||
### 2. Enhanced User Experience
|
||||
|
||||
- **Loading States**
|
||||
- Progress indicators for long operations
|
||||
- Skeleton loading for content
|
||||
- Smooth transitions between states
|
||||
|
||||
- **Feedback Mechanisms**
|
||||
- Toast notifications for actions
|
||||
- Error messages with recovery options
|
||||
- Success confirmations
|
||||
|
||||
- **Responsive Design**
|
||||
- Mobile-friendly layouts
|
||||
- Adaptive column systems
|
||||
- Flexible containers
|
||||
|
||||
### 3. Performance Optimizations
|
||||
|
||||
- **State Management**
|
||||
- Centralized state handling
|
||||
- Efficient data persistence
|
||||
- Optimized re-rendering
|
||||
|
||||
- **Resource Loading**
|
||||
- Lazy loading of components
|
||||
- Optimized image loading
|
||||
- Cached computations
|
||||
|
||||
### 4. Accessibility Features
|
||||
|
||||
- **Keyboard Navigation**
|
||||
- Focus management
|
||||
- Keyboard shortcuts
|
||||
- ARIA labels
|
||||
|
||||
- **Visual Accessibility**
|
||||
- High contrast themes
|
||||
- Screen reader support
|
||||
- Color blind friendly
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
- **Graceful Degradation**
|
||||
- Fallback UI components
|
||||
- Error boundaries
|
||||
- Recovery options
|
||||
|
||||
- **User Feedback**
|
||||
- Clear error messages
|
||||
- Actionable suggestions
|
||||
- Help documentation
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Component Usage
|
||||
|
||||
```python
|
||||
from twitter_streamlit_ui.components.cards import FeatureCard
|
||||
from twitter_streamlit_ui.components.forms import TweetForm
|
||||
from twitter_streamlit_ui.styles.theme import apply_theme
|
||||
|
||||
# Apply theme
|
||||
apply_theme()
|
||||
|
||||
# Use components
|
||||
feature_card = FeatureCard(
|
||||
title="Tweet Generator",
|
||||
description="Create engaging tweets with AI",
|
||||
icon="🐦"
|
||||
)
|
||||
feature_card.render()
|
||||
|
||||
tweet_form = TweetForm()
|
||||
tweet_form.render()
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```python
|
||||
from twitter_streamlit_ui.utils.state import StateManager
|
||||
|
||||
# Initialize state
|
||||
state = StateManager()
|
||||
state.initialize()
|
||||
|
||||
# Update state
|
||||
state.update("current_tweet", tweet_data)
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```python
|
||||
from twitter_streamlit_ui.components.feedback import ErrorBoundary
|
||||
|
||||
with ErrorBoundary():
|
||||
# Your code here
|
||||
pass
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Component Reusability**
|
||||
- Use existing components when possible
|
||||
- Create new components only when necessary
|
||||
- Follow the established patterns
|
||||
|
||||
2. **State Management**
|
||||
- Use the StateManager for all state
|
||||
- Avoid direct session state manipulation
|
||||
- Keep state updates atomic
|
||||
|
||||
3. **Performance**
|
||||
- Use lazy loading for heavy components
|
||||
- Implement caching where appropriate
|
||||
- Monitor render performance
|
||||
|
||||
4. **Accessibility**
|
||||
- Include ARIA labels
|
||||
- Ensure keyboard navigation
|
||||
- Test with screen readers
|
||||
|
||||
5. **Error Handling**
|
||||
- Use ErrorBoundary components
|
||||
- Provide clear error messages
|
||||
- Include recovery options
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Component Library**
|
||||
- Add more specialized components
|
||||
- Enhance existing components
|
||||
- Create component documentation
|
||||
|
||||
2. **Theme System**
|
||||
- Add more theme options
|
||||
- Implement theme switching
|
||||
- Create custom theme builder
|
||||
|
||||
3. **Performance**
|
||||
- Implement virtual scrolling
|
||||
- Add performance monitoring
|
||||
- Optimize resource loading
|
||||
|
||||
4. **Testing**
|
||||
- Add component tests
|
||||
- Implement E2E tests
|
||||
- Create test documentation
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Follow the established patterns
|
||||
2. Add tests for new components
|
||||
3. Update documentation
|
||||
4. Ensure accessibility
|
||||
5. Optimize performance
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
Twitter Streamlit UI package.
|
||||
Provides a modern and user-friendly interface for Twitter tools.
|
||||
"""
|
||||
|
||||
from .dashboard import TwitterDashboard
|
||||
from .components.cards import FeatureCard, TweetCard
|
||||
from .components.forms import TweetForm, SettingsForm
|
||||
from .components.navigation import Sidebar, Header, Tabs, Breadcrumbs
|
||||
from .styles.theme import Theme
|
||||
from .utils.helpers import (
|
||||
save_to_session,
|
||||
get_from_session,
|
||||
clear_session,
|
||||
save_to_file,
|
||||
load_from_file,
|
||||
format_datetime,
|
||||
parse_datetime,
|
||||
validate_tweet_content,
|
||||
validate_hashtags,
|
||||
validate_emojis,
|
||||
calculate_engagement_score,
|
||||
generate_tweet_metrics,
|
||||
copy_to_clipboard,
|
||||
show_success_message,
|
||||
show_error_message,
|
||||
show_info_message,
|
||||
show_warning_message,
|
||||
create_download_button,
|
||||
create_upload_button
|
||||
)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "AI Writer Team"
|
||||
|
||||
__all__ = [
|
||||
"TwitterDashboard",
|
||||
"FeatureCard",
|
||||
"TweetCard",
|
||||
"TweetForm",
|
||||
"SettingsForm",
|
||||
"Sidebar",
|
||||
"Header",
|
||||
"Tabs",
|
||||
"Breadcrumbs",
|
||||
"Theme",
|
||||
"save_to_session",
|
||||
"get_from_session",
|
||||
"clear_session",
|
||||
"save_to_file",
|
||||
"load_from_file",
|
||||
"format_datetime",
|
||||
"parse_datetime",
|
||||
"validate_tweet_content",
|
||||
"validate_hashtags",
|
||||
"validate_emojis",
|
||||
"calculate_engagement_score",
|
||||
"generate_tweet_metrics",
|
||||
"copy_to_clipboard",
|
||||
"show_success_message",
|
||||
"show_error_message",
|
||||
"show_info_message",
|
||||
"show_warning_message",
|
||||
"create_download_button",
|
||||
"create_upload_button"
|
||||
]
|
||||
@@ -1,634 +0,0 @@
|
||||
"""
|
||||
Enhanced UI Cards with modern styling and improved functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, List, Optional, Callable
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from datetime import datetime
|
||||
|
||||
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: 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.stats = stats or {}
|
||||
self.actions = actions or []
|
||||
self.on_click = on_click
|
||||
|
||||
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>
|
||||
""")
|
||||
stats_html = f"""
|
||||
<div class="feature-stats">
|
||||
{''.join(stats_items)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 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:
|
||||
"""Modern tweet card component."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: str,
|
||||
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
|
||||
):
|
||||
self.content = content
|
||||
self.engagement_score = engagement_score
|
||||
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 _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>
|
||||
"""
|
||||
|
||||
# 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,554 +0,0 @@
|
||||
"""
|
||||
Enhanced Navigation Component for Twitter UI with modern styling and improved functionality.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
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 = "Navigation", logo: Optional[str] = None):
|
||||
"""Initialize the sidebar."""
|
||||
self.title = title
|
||||
self.logo = logo
|
||||
self.menu_items = []
|
||||
|
||||
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,
|
||||
'key': key,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def render(self) -> str:
|
||||
"""Render the sidebar and return the selected page."""
|
||||
with st.sidebar:
|
||||
# Logo and title
|
||||
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:
|
||||
if st.button(
|
||||
f"{item['icon']} {item['label']}",
|
||||
key=f"sidebar_{item['key']}",
|
||||
use_container_width=True
|
||||
):
|
||||
selected_page = item['key']
|
||||
if item.get('callback'):
|
||||
item['callback']()
|
||||
|
||||
return selected_page or st.session_state.get('current_page', 'dashboard')
|
||||
|
||||
|
||||
class Header:
|
||||
"""Header component with title and actions."""
|
||||
|
||||
def __init__(self, title: str = "Dashboard", subtitle: str = ""):
|
||||
"""Initialize the header."""
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.actions = []
|
||||
|
||||
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': help_text
|
||||
})
|
||||
|
||||
def render(self):
|
||||
"""Render the header."""
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
st.title(f"{self.title}")
|
||||
if self.subtitle:
|
||||
st.markdown(f"*{self.subtitle}*")
|
||||
|
||||
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):
|
||||
"""Initialize the tabs."""
|
||||
self.tabs = []
|
||||
|
||||
def add_tab(self, label: str, icon: str, content_func: Callable):
|
||||
"""Add a tab."""
|
||||
self.tabs.append({
|
||||
'label': label,
|
||||
'icon': icon,
|
||||
'content_func': content_func
|
||||
})
|
||||
|
||||
def render(self):
|
||||
"""Render the tabs."""
|
||||
if not self.tabs:
|
||||
return
|
||||
|
||||
tab_labels = [f"{tab['icon']} {tab['label']}" for tab in self.tabs]
|
||||
selected_tabs = st.tabs(tab_labels)
|
||||
|
||||
for i, tab in enumerate(self.tabs):
|
||||
with selected_tabs[i]:
|
||||
tab['content_func']()
|
||||
|
||||
|
||||
class Breadcrumbs:
|
||||
"""Breadcrumb navigation component."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize breadcrumbs."""
|
||||
self.items = []
|
||||
|
||||
def add_item(self, label: str, key: str = None, callback: Callable = None):
|
||||
"""Add a breadcrumb item."""
|
||||
self.items.append({
|
||||
'label': label,
|
||||
'key': key,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def render(self):
|
||||
"""Render the breadcrumbs."""
|
||||
if not self.items:
|
||||
return
|
||||
|
||||
breadcrumb_html = '<div class="nav-breadcrumb">'
|
||||
|
||||
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'
|
||||
}
|
||||
]
|
||||
@@ -1,278 +0,0 @@
|
||||
"""
|
||||
Main dashboard for Twitter UI.
|
||||
Combines all UI components into a cohesive interface.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, Optional
|
||||
from .components.cards import FeatureCard, TweetCard
|
||||
from .components.forms import TweetForm, SettingsForm
|
||||
from .components.navigation import Sidebar, Header, Tabs, Breadcrumbs
|
||||
from .styles.theme import Theme
|
||||
import os
|
||||
|
||||
class TwitterDashboard:
|
||||
"""Main dashboard class for Twitter UI."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Twitter dashboard."""
|
||||
self.setup_theme()
|
||||
self.setup_navigation()
|
||||
self.setup_state()
|
||||
|
||||
def get_logo_path(self) -> str:
|
||||
"""Get the best available logo path with fallbacks."""
|
||||
# List of potential logo paths in order of preference
|
||||
logo_paths = [
|
||||
"lib/workspace/alwrity_logo.png",
|
||||
"lib/workspace/AskAlwrity-min.ico",
|
||||
"lib/workspace/alwrity_ai_writer.png"
|
||||
]
|
||||
|
||||
for path in logo_paths:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
# If no logo files are found, return None
|
||||
return None
|
||||
|
||||
def setup_theme(self) -> None:
|
||||
"""Setup theme and styling."""
|
||||
Theme.apply()
|
||||
|
||||
def setup_navigation(self) -> None:
|
||||
"""Setup navigation components."""
|
||||
# Sidebar
|
||||
self.sidebar = Sidebar(
|
||||
title="Twitter Tools",
|
||||
logo=self.get_logo_path()
|
||||
)
|
||||
|
||||
# Add menu items
|
||||
self.sidebar.add_menu_item("Dashboard", "📊", "dashboard")
|
||||
self.sidebar.add_menu_item("Tweet Generator", "✍️", "tweet_generator")
|
||||
self.sidebar.add_menu_item("Analytics", "📈", "analytics")
|
||||
self.sidebar.add_menu_item("Settings", "⚙️", "settings")
|
||||
|
||||
# Header
|
||||
self.header = Header(
|
||||
title="Twitter Dashboard",
|
||||
subtitle="Create and manage your Twitter content"
|
||||
)
|
||||
|
||||
# Add header actions
|
||||
self.header.add_action(
|
||||
"New Tweet",
|
||||
"✏️",
|
||||
self.create_new_tweet,
|
||||
"Create a new tweet"
|
||||
)
|
||||
self.header.add_action(
|
||||
"Refresh",
|
||||
"🔄",
|
||||
self.refresh_dashboard,
|
||||
"Refresh dashboard data"
|
||||
)
|
||||
|
||||
# Tabs
|
||||
self.tabs = Tabs()
|
||||
|
||||
# Add tabs
|
||||
self.tabs.add_tab("Overview", "📊", self.render_overview)
|
||||
self.tabs.add_tab("Recent Tweets", "🐦", self.render_recent_tweets)
|
||||
self.tabs.add_tab("Analytics", "📈", self.render_analytics)
|
||||
|
||||
# Breadcrumbs
|
||||
self.breadcrumbs = Breadcrumbs()
|
||||
|
||||
def setup_state(self) -> None:
|
||||
"""Initialize session state variables."""
|
||||
if "current_page" not in st.session_state:
|
||||
st.session_state["current_page"] = "dashboard"
|
||||
if "current_tab" not in st.session_state:
|
||||
st.session_state["current_tab"] = "Overview"
|
||||
if "tweets" not in st.session_state:
|
||||
st.session_state["tweets"] = []
|
||||
|
||||
def create_new_tweet(self) -> None:
|
||||
"""Handle new tweet creation."""
|
||||
st.session_state["current_page"] = "tweet_generator"
|
||||
|
||||
def refresh_dashboard(self) -> None:
|
||||
"""Refresh dashboard data."""
|
||||
st.rerun()
|
||||
|
||||
def render_overview(self) -> None:
|
||||
"""Render the overview tab content."""
|
||||
# Feature cards
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
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=self.create_new_tweet
|
||||
).render()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
def render_recent_tweets(self) -> None:
|
||||
"""Render the recent tweets tab content."""
|
||||
# Tweet form
|
||||
tweet_form = TweetForm(
|
||||
on_submit=self.handle_tweet_submit
|
||||
)
|
||||
tweet_form.render()
|
||||
|
||||
# Recent tweets
|
||||
st.markdown("### Recent Tweets")
|
||||
|
||||
for tweet in st.session_state["tweets"]:
|
||||
TweetCard(
|
||||
content=tweet["content"],
|
||||
engagement_score=tweet["engagement_score"],
|
||||
hashtags=tweet["hashtags"],
|
||||
emojis=tweet["emojis"],
|
||||
metrics=tweet["metrics"],
|
||||
on_copy=lambda: self.copy_tweet(tweet),
|
||||
on_save=lambda: self.save_tweet(tweet)
|
||||
).render()
|
||||
|
||||
def render_analytics(self) -> None:
|
||||
"""Render the analytics tab content."""
|
||||
# Analytics content
|
||||
st.markdown("### Tweet Analytics")
|
||||
|
||||
# Placeholder for analytics charts
|
||||
st.info("Analytics features coming soon!")
|
||||
|
||||
def handle_tweet_submit(self) -> None:
|
||||
"""Handle tweet form submission."""
|
||||
# Get form data
|
||||
content = st.session_state["tweet_content"]
|
||||
tone = st.session_state["tone"]
|
||||
length = st.session_state["length"]
|
||||
hashtags = st.session_state["hashtags"]
|
||||
emojis = st.session_state["emojis"]
|
||||
engagement_boost = st.session_state["engagement_boost"]
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
# Add to tweets list
|
||||
st.session_state["tweets"].append(tweet)
|
||||
|
||||
# Show success message
|
||||
st.success("Tweet created successfully!")
|
||||
|
||||
def copy_tweet(self, tweet: Dict[str, Any]) -> None:
|
||||
"""Copy tweet to clipboard."""
|
||||
st.write("Tweet copied to clipboard!")
|
||||
|
||||
def save_tweet(self, tweet: Dict[str, Any]) -> None:
|
||||
"""Save tweet for later."""
|
||||
st.write("Tweet saved!")
|
||||
|
||||
def render(self) -> None:
|
||||
"""Render the complete dashboard."""
|
||||
# Render navigation
|
||||
self.sidebar.render()
|
||||
self.header.render()
|
||||
self.breadcrumbs.render()
|
||||
|
||||
# Render content based on current page
|
||||
if st.session_state["current_page"] == "dashboard":
|
||||
self.tabs.render()
|
||||
elif st.session_state["current_page"] == "tweet_generator":
|
||||
self.render_recent_tweets()
|
||||
elif st.session_state["current_page"] == "analytics":
|
||||
self.render_analytics()
|
||||
elif st.session_state["current_page"] == "settings":
|
||||
settings_form = SettingsForm(
|
||||
on_submit=self.handle_settings_submit
|
||||
)
|
||||
settings_form.render()
|
||||
|
||||
def handle_settings_submit(self) -> None:
|
||||
"""Handle settings form submission."""
|
||||
# Get form data
|
||||
api_key = st.session_state["api_key"]
|
||||
theme = st.session_state["theme"]
|
||||
notifications = st.session_state["notifications"]
|
||||
auto_save = st.session_state["auto_save"]
|
||||
language = st.session_state["language"]
|
||||
|
||||
# Save settings
|
||||
st.session_state["settings"] = {
|
||||
"api_key": api_key,
|
||||
"theme": theme,
|
||||
"notifications": notifications,
|
||||
"auto_save": auto_save,
|
||||
"language": language
|
||||
}
|
||||
|
||||
# Show success message
|
||||
st.success("Settings saved successfully!")
|
||||
|
||||
def main():
|
||||
"""Main entry point for the dashboard."""
|
||||
dashboard = TwitterDashboard()
|
||||
dashboard.render()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,173 +0,0 @@
|
||||
"""
|
||||
Theme configuration for Twitter UI components.
|
||||
Provides consistent styling across all Twitter-related features.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any
|
||||
|
||||
class Theme:
|
||||
"""Theme configuration for Twitter UI components."""
|
||||
|
||||
# Color palette
|
||||
COLORS = {
|
||||
"primary": "#1DA1F2", # Twitter blue
|
||||
"secondary": "#14171A", # Dark blue
|
||||
"background": "#15202B", # Dark background
|
||||
"text": "#FFFFFF", # White text
|
||||
"text_secondary": "#8899A6", # Gray text
|
||||
"success": "#17BF63", # Green
|
||||
"warning": "#FFAD1F", # Yellow
|
||||
"error": "#E0245E", # Red
|
||||
"border": "rgba(255, 255, 255, 0.1)", # Subtle border
|
||||
}
|
||||
|
||||
# Typography
|
||||
TYPOGRAPHY = {
|
||||
"font_family": "'Helvetica Neue', sans-serif",
|
||||
"font_sizes": {
|
||||
"h1": "2.5rem",
|
||||
"h2": "2rem",
|
||||
"h3": "1.5rem",
|
||||
"body": "1rem",
|
||||
"small": "0.875rem",
|
||||
},
|
||||
"font_weights": {
|
||||
"regular": 400,
|
||||
"medium": 500,
|
||||
"bold": 700,
|
||||
},
|
||||
}
|
||||
|
||||
# Spacing
|
||||
SPACING = {
|
||||
"xs": "0.25rem",
|
||||
"sm": "0.5rem",
|
||||
"md": "1rem",
|
||||
"lg": "1.5rem",
|
||||
"xl": "2rem",
|
||||
}
|
||||
|
||||
# Border radius
|
||||
BORDER_RADIUS = {
|
||||
"sm": "4px",
|
||||
"md": "8px",
|
||||
"lg": "12px",
|
||||
"xl": "16px",
|
||||
"full": "9999px",
|
||||
}
|
||||
|
||||
# Shadows
|
||||
SHADOWS = {
|
||||
"sm": "0 1px 2px rgba(0, 0, 0, 0.05)",
|
||||
"md": "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
"lg": "0 10px 15px rgba(0, 0, 0, 0.1)",
|
||||
"xl": "0 20px 25px rgba(0, 0, 0, 0.15)",
|
||||
}
|
||||
|
||||
# Transitions
|
||||
TRANSITIONS = {
|
||||
"fast": "0.15s ease",
|
||||
"normal": "0.3s ease",
|
||||
"slow": "0.5s ease",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_css(cls) -> str:
|
||||
"""Get the complete CSS for the theme."""
|
||||
return f"""
|
||||
/* Base styles */
|
||||
.stApp {{
|
||||
background-color: {cls.COLORS['background']};
|
||||
color: {cls.COLORS['text']};
|
||||
font-family: {cls.TYPOGRAPHY['font_family']};
|
||||
}}
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
color: {cls.COLORS['text']};
|
||||
font-family: {cls.TYPOGRAPHY['font_family']};
|
||||
font-weight: {cls.TYPOGRAPHY['font_weights']['bold']};
|
||||
}}
|
||||
|
||||
/* Buttons */
|
||||
.stButton > button {{
|
||||
background: linear-gradient(45deg, {cls.COLORS['primary']}, #0C85D0);
|
||||
color: {cls.COLORS['text']};
|
||||
border: none;
|
||||
padding: {cls.SPACING['md']} {cls.SPACING['lg']};
|
||||
border-radius: {cls.BORDER_RADIUS['full']};
|
||||
font-weight: {cls.TYPOGRAPHY['font_weights']['medium']};
|
||||
transition: all {cls.TRANSITIONS['normal']};
|
||||
box-shadow: {cls.SHADOWS['md']};
|
||||
}}
|
||||
|
||||
.stButton > button:hover {{
|
||||
transform: translateY(-2px);
|
||||
box-shadow: {cls.SHADOWS['lg']};
|
||||
}}
|
||||
|
||||
/* Cards */
|
||||
.card {{
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid {cls.COLORS['border']};
|
||||
border-radius: {cls.BORDER_RADIUS['lg']};
|
||||
padding: {cls.SPACING['lg']};
|
||||
margin-bottom: {cls.SPACING['md']};
|
||||
backdrop-filter: blur(10px);
|
||||
transition: transform {cls.TRANSITIONS['normal']};
|
||||
}}
|
||||
|
||||
.card:hover {{
|
||||
transform: translateY(-4px);
|
||||
}}
|
||||
|
||||
/* Forms */
|
||||
.stTextInput > div > div > input {{
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid {cls.COLORS['border']};
|
||||
border-radius: {cls.BORDER_RADIUS['md']};
|
||||
color: {cls.COLORS['text']};
|
||||
padding: {cls.SPACING['md']};
|
||||
}}
|
||||
|
||||
/* Tabs */
|
||||
.stTabs [data-baseweb="tab-list"] {{
|
||||
gap: {cls.SPACING['sm']};
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: {cls.SPACING['md']};
|
||||
border-radius: {cls.BORDER_RADIUS['lg']};
|
||||
}}
|
||||
|
||||
.stTabs [data-baseweb="tab"] {{
|
||||
background-color: transparent;
|
||||
color: {cls.COLORS['text']};
|
||||
border: 1px solid {cls.COLORS['border']};
|
||||
border-radius: {cls.BORDER_RADIUS['md']};
|
||||
padding: {cls.SPACING['sm']} {cls.SPACING['md']};
|
||||
}}
|
||||
|
||||
/* Status badges */
|
||||
.status-badge {{
|
||||
display: inline-block;
|
||||
padding: {cls.SPACING['xs']} {cls.SPACING['md']};
|
||||
border-radius: {cls.BORDER_RADIUS['full']};
|
||||
font-size: {cls.TYPOGRAPHY['font_sizes']['small']};
|
||||
font-weight: {cls.TYPOGRAPHY['font_weights']['medium']};
|
||||
}}
|
||||
|
||||
.status-active {{
|
||||
background: linear-gradient(45deg, {cls.COLORS['success']}, #69F0AE);
|
||||
color: {cls.COLORS['secondary']};
|
||||
}}
|
||||
|
||||
.status-coming-soon {{
|
||||
background: linear-gradient(45deg, {cls.COLORS['warning']}, #FFA000);
|
||||
color: {cls.COLORS['secondary']};
|
||||
}}
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def apply(cls) -> None:
|
||||
"""Apply the theme to the Streamlit app."""
|
||||
st.markdown(f"<style>{cls.get_css()}</style>", unsafe_allow_html=True)
|
||||
@@ -1,503 +0,0 @@
|
||||
"""
|
||||
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()
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
Utility functions for Twitter UI.
|
||||
Provides helper functions for common operations.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
from typing import Dict, Any, List, Optional
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
def save_to_session(key: str, value: Any) -> None:
|
||||
"""Save a value to the session state."""
|
||||
st.session_state[key] = value
|
||||
|
||||
def get_from_session(key: str, default: Any = None) -> Any:
|
||||
"""Get a value from the session state."""
|
||||
return st.session_state.get(key, default)
|
||||
|
||||
def clear_session() -> None:
|
||||
"""Clear all session state variables."""
|
||||
for key in list(st.session_state.keys()):
|
||||
del st.session_state[key]
|
||||
|
||||
def save_to_file(data: Dict[str, Any], filename: str) -> None:
|
||||
"""Save data to a JSON file."""
|
||||
try:
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(data, f, indent=4)
|
||||
except Exception as e:
|
||||
st.error(f"Error saving data: {str(e)}")
|
||||
|
||||
def load_from_file(filename: str) -> Optional[Dict[str, Any]]:
|
||||
"""Load data from a JSON file."""
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
with open(filename, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
st.error(f"Error loading data: {str(e)}")
|
||||
return None
|
||||
|
||||
def format_datetime(dt: datetime) -> str:
|
||||
"""Format a datetime object for display."""
|
||||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def parse_datetime(dt_str: str) -> Optional[datetime]:
|
||||
"""Parse a datetime string."""
|
||||
try:
|
||||
return datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def validate_tweet_content(content: str) -> bool:
|
||||
"""Validate tweet content."""
|
||||
if not content:
|
||||
st.error("Tweet content cannot be empty")
|
||||
return False
|
||||
if len(content) > 280:
|
||||
st.error("Tweet content cannot exceed 280 characters")
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_hashtags(hashtags: List[str]) -> bool:
|
||||
"""Validate hashtags."""
|
||||
for tag in hashtags:
|
||||
if not tag.startswith('#'):
|
||||
st.error(f"Hashtag {tag} must start with #")
|
||||
return False
|
||||
if len(tag) > 30:
|
||||
st.error(f"Hashtag {tag} cannot exceed 30 characters")
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_emojis(emojis: List[str]) -> bool:
|
||||
"""Validate emojis."""
|
||||
for emoji in emojis:
|
||||
if len(emoji) != 1:
|
||||
st.error(f"Invalid emoji: {emoji}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def calculate_engagement_score(
|
||||
content: str,
|
||||
hashtags: List[str],
|
||||
emojis: List[str],
|
||||
tone: str
|
||||
) -> float:
|
||||
"""Calculate engagement score for a tweet."""
|
||||
score = 0.0
|
||||
|
||||
# Content length score (optimal length is 100-150 characters)
|
||||
content_length = len(content)
|
||||
if 100 <= content_length <= 150:
|
||||
score += 30
|
||||
elif 50 <= content_length <= 200:
|
||||
score += 20
|
||||
else:
|
||||
score += 10
|
||||
|
||||
# Hashtag score (optimal number is 2-3 hashtags)
|
||||
hashtag_count = len(hashtags)
|
||||
if 2 <= hashtag_count <= 3:
|
||||
score += 20
|
||||
elif 1 <= hashtag_count <= 4:
|
||||
score += 15
|
||||
else:
|
||||
score += 5
|
||||
|
||||
# Emoji score (optimal number is 1-2 emojis)
|
||||
emoji_count = len(emojis)
|
||||
if 1 <= emoji_count <= 2:
|
||||
score += 20
|
||||
elif 0 <= emoji_count <= 3:
|
||||
score += 15
|
||||
else:
|
||||
score += 5
|
||||
|
||||
# Tone score
|
||||
tone_scores = {
|
||||
"professional": 15,
|
||||
"casual": 20,
|
||||
"humorous": 25,
|
||||
"informative": 15,
|
||||
"inspirational": 20
|
||||
}
|
||||
score += tone_scores.get(tone, 10)
|
||||
|
||||
return min(score, 100)
|
||||
|
||||
def generate_tweet_metrics(engagement_score: float) -> Dict[str, float]:
|
||||
"""Generate metrics for a tweet based on engagement score."""
|
||||
return {
|
||||
"Engagement": engagement_score,
|
||||
"Reach": engagement_score * 0.8,
|
||||
"Growth": engagement_score * 0.6
|
||||
}
|
||||
|
||||
def copy_to_clipboard(text: str) -> None:
|
||||
"""Copy text to clipboard."""
|
||||
try:
|
||||
st.write(f'<script>navigator.clipboard.writeText("{text}")</script>', unsafe_allow_html=True)
|
||||
except Exception as e:
|
||||
st.error(f"Error copying to clipboard: {str(e)}")
|
||||
|
||||
def show_success_message(message: str) -> None:
|
||||
"""Show a success message."""
|
||||
st.success(message)
|
||||
|
||||
def show_error_message(message: str) -> None:
|
||||
"""Show an error message."""
|
||||
st.error(message)
|
||||
|
||||
def show_info_message(message: str) -> None:
|
||||
"""Show an info message."""
|
||||
st.info(message)
|
||||
|
||||
def show_warning_message(message: str) -> None:
|
||||
"""Show a warning message."""
|
||||
st.warning(message)
|
||||
|
||||
def create_download_button(
|
||||
data: Dict[str, Any],
|
||||
filename: str,
|
||||
button_text: str = "Download"
|
||||
) -> None:
|
||||
"""Create a download button for data."""
|
||||
try:
|
||||
json_str = json.dumps(data, indent=4)
|
||||
st.download_button(
|
||||
label=button_text,
|
||||
data=json_str,
|
||||
file_name=filename,
|
||||
mime="application/json"
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Error creating download button: {str(e)}")
|
||||
|
||||
def create_upload_button(
|
||||
on_upload: callable,
|
||||
button_text: str = "Upload",
|
||||
file_types: List[str] = ["json"]
|
||||
) -> None:
|
||||
"""Create an upload button for data."""
|
||||
try:
|
||||
uploaded_file = st.file_uploader(
|
||||
button_text,
|
||||
type=file_types
|
||||
)
|
||||
if uploaded_file is not None:
|
||||
data = json.load(uploaded_file)
|
||||
on_upload(data)
|
||||
except Exception as e:
|
||||
st.error(f"Error handling upload: {str(e)}")
|
||||
@@ -1,121 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
from textwrap import dedent
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import streamlit as st
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(Path('../../.env'))
|
||||
from loguru import logger
|
||||
logger.remove()
|
||||
logger.add(sys.stdout,
|
||||
colorize=True,
|
||||
format="<level>{level}</level>|<green>{file}:{line}:{function}</green>| {message}"
|
||||
)
|
||||
|
||||
from ..ai_web_researcher.firecrawl_web_crawler import scrape_url
|
||||
from ..blog_metadata.get_blog_metadata import blog_metadata, run_async
|
||||
from ..blog_postprocessing.save_blog_to_file import save_blog_to_file
|
||||
from ..gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
from ..gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def blog_from_url(weburl):
|
||||
"""
|
||||
This function will take a blog Topic to first generate sections for it
|
||||
and then generate content for each section.
|
||||
"""
|
||||
# Use to store the blog in a string, to save in a *.md file.
|
||||
blog_markdown_str = None
|
||||
tavily_search_result = None
|
||||
# Initializing the variables
|
||||
blog_title = None
|
||||
blog_meta_desc = None
|
||||
blog_tags = None
|
||||
blog_categories = None
|
||||
|
||||
logger.info(f"Researching and Writing Blog on: {weburl}")
|
||||
with st.status("Started Writing..", expanded=True) as status:
|
||||
st.empty()
|
||||
status.update(label=f"Researching and Writing Blog on: {weburl}")
|
||||
try:
|
||||
scraped_text = scrape_url(weburl)
|
||||
#logger.info(scraped_text)
|
||||
except Exception as err:
|
||||
st.error(f"Failed to scrape web page from url-{weburl} - Error: {err}")
|
||||
logger.error(f"Failed in web research: {err}")
|
||||
st.stop()
|
||||
status.update(label=f"Successfully Scraped/Fetched url: {weburl}", expanded=False, state="complete")
|
||||
|
||||
with st.status(f"Started Writing blog from {weburl}..", expanded=True) as status:
|
||||
# Do Tavily AI research to augument the above blog.
|
||||
try:
|
||||
blog_markdown_str = write_blog_from_weburl(scraped_text)
|
||||
status.update(label="Finished Writing Blog From: {weburl}")
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to write blog from: {weburl}")
|
||||
st.error(f"Failed to write blog from: {weburl}")
|
||||
st.stop()
|
||||
|
||||
try:
|
||||
status.update(label="🙎 Generating - Title, Meta Description, Tags, Categories for the content.")
|
||||
blog_title, blog_meta_desc, blog_tags, blog_categories = run_async(blog_metadata(blog_markdown_str))
|
||||
except Exception as err:
|
||||
st.error(f"Failed to get blog metadata: {err}")
|
||||
|
||||
try:
|
||||
status.update(label="🙎 Generating Image for the new blog.")
|
||||
generated_image_filepath = generate_image(f"{blog_title} + ' ' + {blog_meta_desc}")
|
||||
except Exception as err:
|
||||
st.warning(f"Failed in Image generation: {err}")
|
||||
|
||||
saved_blog_to_file = save_blog_to_file(blog_markdown_str, blog_title, blog_meta_desc,
|
||||
blog_tags, blog_categories, generated_image_filepath)
|
||||
status.update(label=f"Saved the content in this file: {saved_blog_to_file}")
|
||||
|
||||
logger.info(f"\n\n --------- Finished writing Blog for : {weburl} -------------- \n")
|
||||
if generated_image_filepath:
|
||||
st.image(generated_image_filepath)
|
||||
|
||||
st.markdown(f"{blog_markdown_str}")
|
||||
status.update(label=f"Finished, Review & Use your Original Content Below: {saved_blog_to_file}", state="complete")
|
||||
|
||||
|
||||
def write_blog_from_weburl(scraped_website):
|
||||
"""Combine the given online research and GPT blog content"""
|
||||
try:
|
||||
config_path = Path(os.environ["ALWRITY_CONFIG"])
|
||||
with open(config_path, 'r', encoding='utf-8') as file:
|
||||
config = json.load(file)
|
||||
except Exception as err:
|
||||
logger.error(f"Error: Failed to read values from config: {err}")
|
||||
exit(1)
|
||||
|
||||
blog_characteristics = config['Blog Content Characteristics']
|
||||
|
||||
prompt = f"""
|
||||
As expert Creative Content writer, I will provide you with scraped website content.
|
||||
I want you to write a detailed {blog_characteristics['Blog Type']} blog post including 5 FAQs.
|
||||
|
||||
Below are the guidelines to follow:
|
||||
1). You must respond in {blog_characteristics['Blog Language']} language.
|
||||
2). Tone and Brand Alignment: Adjust your tone, voice, personality for {blog_characteristics['Blog Tone']} audience.
|
||||
3). Make sure your response content length is of {blog_characteristics['Blog Length']} words.
|
||||
4). Include FAQs from 'People also Ask' section of provided context 'google search result'.
|
||||
|
||||
I want the post to offer unique insights, relatable examples, and a fresh perspective on the topic.
|
||||
\n\n
|
||||
Website Content:
|
||||
'''{scraped_website}'''
|
||||
"""
|
||||
logger.info("Generating blog and FAQs from Google web search results.")
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Exit: Failed to get response from LLM: {err}")
|
||||
exit(1)
|
||||
Reference in New Issue
Block a user