ALwrity Version 0.5.1 (Fastapi + React)
This commit is contained in:
169
ToBeMigrated/alwrity_ui/alwrity_researcher/README.md
Normal file
169
ToBeMigrated/alwrity_ui/alwrity_researcher/README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# AI Web Researcher Dashboard for Content Creators
|
||||
|
||||
## Overview
|
||||
|
||||
The AI Web Researcher Dashboard is a comprehensive suite of research tools designed specifically for content creators. This dashboard integrates various web research modules to streamline the content research process, enhance content quality, and improve workflow efficiency.
|
||||
|
||||
## Available Research Modules
|
||||
|
||||
### 1. Search Engine Research Tools
|
||||
|
||||
#### Google SERP Search
|
||||
- **Functionality**: Retrieves organic search results, People Also Ask questions, and related searches
|
||||
- **Best for**: Initial topic research, understanding search intent, and identifying content gaps
|
||||
- **Key features**: Comprehensive search results with structured data extraction
|
||||
|
||||
#### Tavily AI Search
|
||||
- **Functionality**: Advanced AI-powered search with semantic understanding
|
||||
- **Best for**: In-depth research requiring contextual understanding
|
||||
- **Key features**: Provides direct answers to questions and follow-up question suggestions
|
||||
|
||||
### 2. Neural Search Tools
|
||||
|
||||
#### Metaphor Neural Search
|
||||
- **Functionality**: Semantic search technology for finding related content
|
||||
- **Best for**: Discovering content based on conceptual similarity rather than keyword matching
|
||||
- **Key features**: Find similar articles, competitor analysis, and content inspiration
|
||||
|
||||
### 3. Trend Analysis Tools
|
||||
|
||||
#### Google Trends Researcher
|
||||
- **Functionality**: Analyzes search term popularity over time
|
||||
- **Best for**: Content planning, seasonal topic identification, and trend forecasting
|
||||
- **Key features**: Interest over time visualization, regional interest mapping, and related query analysis
|
||||
|
||||
### 4. Web Crawling & Analysis Tools
|
||||
|
||||
#### Async Web Crawler
|
||||
- **Functionality**: Crawls websites to extract structured content
|
||||
- **Best for**: Content auditing, competitor analysis, and data extraction
|
||||
- **Key features**: Extracts titles, descriptions, main content, headings, links, and images
|
||||
|
||||
#### Firecrawl Web Crawler
|
||||
- **Functionality**: Specialized crawler for single-page or website scraping
|
||||
- **Best for**: Detailed content extraction from specific URLs
|
||||
- **Key features**: Configurable depth and page limits for targeted crawling
|
||||
|
||||
#### Website Analyzer
|
||||
- **Functionality**: Comprehensive website analysis for content and technical aspects
|
||||
- **Best for**: Content quality assessment, SEO analysis, and technical audits
|
||||
- **Key features**: Content quality metrics, SEO recommendations, and technical performance insights
|
||||
|
||||
## Optimal Workflows for Content Creators
|
||||
|
||||
### Research Workflow 1: Comprehensive Topic Research
|
||||
|
||||
1. **Initial Exploration** (Google SERP Search)
|
||||
- Search for your main topic to understand the search landscape
|
||||
- Analyze People Also Ask questions to identify user intent
|
||||
|
||||
2. **In-depth Research** (Tavily AI Search)
|
||||
- Use identified subtopics for deeper research
|
||||
- Collect factual information and expert insights
|
||||
|
||||
3. **Competitive Analysis** (Metaphor Neural Search + Web Crawler)
|
||||
- Find similar content from competitors
|
||||
- Analyze content structure and approach
|
||||
|
||||
4. **Trend Validation** (Google Trends Researcher)
|
||||
- Verify topic popularity and seasonal patterns
|
||||
- Identify related trending topics
|
||||
|
||||
### Research Workflow 2: Content Gap Analysis
|
||||
|
||||
1. **Competitor Content Mapping** (Async Web Crawler)
|
||||
- Crawl competitor websites to extract content structure
|
||||
- Identify their content categories and topics
|
||||
|
||||
2. **Topic Opportunity Discovery** (Google Trends + Metaphor Search)
|
||||
- Research trending topics in your niche
|
||||
- Find content areas with high interest but low competition
|
||||
|
||||
3. **Content Quality Benchmarking** (Website Analyzer)
|
||||
- Analyze top-performing content in your niche
|
||||
- Identify quality benchmarks and content standards
|
||||
|
||||
### Research Workflow 3: Content Refresh & Update
|
||||
|
||||
1. **Content Performance Analysis** (Website Analyzer)
|
||||
- Analyze existing content for improvement opportunities
|
||||
- Identify outdated information and gaps
|
||||
|
||||
2. **Current Trend Integration** (Google Trends Researcher)
|
||||
- Research current trends related to your content
|
||||
- Identify new angles and perspectives
|
||||
|
||||
3. **Competitive Edge Research** (Tavily AI + Metaphor Search)
|
||||
- Research what competitors have updated recently
|
||||
- Find new research, statistics, and information to incorporate
|
||||
|
||||
## Integration with Content Creation Process
|
||||
|
||||
### Pre-Writing Phase
|
||||
- Use research modules to gather comprehensive information
|
||||
- Create content briefs based on research findings
|
||||
- Identify key points, statistics, and examples to include
|
||||
|
||||
### Writing Phase
|
||||
- Reference research findings for accurate information
|
||||
- Use competitor insights to differentiate your content
|
||||
- Incorporate trending topics and keywords
|
||||
|
||||
### Post-Publishing Phase
|
||||
- Monitor content performance against researched benchmarks
|
||||
- Update content based on new research findings
|
||||
- Plan related content based on research insights
|
||||
|
||||
## Dashboard Usage Examples
|
||||
|
||||
### Example 1: Creating a Comprehensive Blog Post
|
||||
|
||||
1. Start with Google SERP Search for your main topic
|
||||
2. Use Tavily AI Search to gather in-depth information on subtopics
|
||||
3. Analyze competitor content with Metaphor Neural Search
|
||||
4. Check topic seasonality with Google Trends Researcher
|
||||
5. Create content that addresses all aspects discovered in research
|
||||
|
||||
### Example 2: Developing a Content Strategy
|
||||
|
||||
1. Use Google Trends to identify trending topics in your niche
|
||||
2. Analyze competitor websites with Async Web Crawler
|
||||
3. Research content gaps using Metaphor Neural Search
|
||||
4. Prioritize content topics based on trend data and competition
|
||||
5. Create a content calendar incorporating research insights
|
||||
|
||||
### Example 3: Updating Existing Content
|
||||
|
||||
1. Analyze current content with Website Analyzer
|
||||
2. Research new developments with Tavily AI Search
|
||||
3. Check for changing trends with Google Trends Researcher
|
||||
4. Identify new angles with Metaphor Neural Search
|
||||
5. Update content with new information and perspectives
|
||||
|
||||
## Best Practices for Effective Research
|
||||
|
||||
1. **Start Broad, Then Narrow**: Begin with general search tools before using specialized ones
|
||||
2. **Combine Multiple Research Methods**: Use different modules for comprehensive insights
|
||||
3. **Focus on User Intent**: Prioritize research that reveals what your audience wants
|
||||
4. **Validate with Data**: Use trend analysis to validate topic importance
|
||||
5. **Organize Research Findings**: Create structured notes from your research
|
||||
6. **Set Research Goals**: Define what you need to learn before starting
|
||||
7. **Schedule Regular Research**: Keep content updated with periodic research
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
- API keys for various services (Google, Tavily, Metaphor, etc.)
|
||||
- Proper configuration in the .env file
|
||||
- Sufficient system resources for web crawling operations
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Integration with content planning calendar
|
||||
- Automated research reports
|
||||
- Competitive content gap analysis
|
||||
- AI-powered content brief generation
|
||||
- Customizable research workflows
|
||||
|
||||
---
|
||||
|
||||
This dashboard is designed to streamline the research process for content creators, providing powerful tools to enhance content quality, relevance, and performance. By following the suggested workflows and best practices, content creators can significantly improve their research efficiency and content outcomes.
|
||||
@@ -0,0 +1,98 @@
|
||||
# AI Web Researcher Dashboard
|
||||
|
||||
## Overview
|
||||
|
||||
The AI Web Researcher Dashboard is a modern, intuitive interface designed specifically for content creators and digital marketing professionals. This dashboard integrates various web research tools to streamline the content research process, enhance content quality, and improve workflow efficiency.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Intuitive User Interface
|
||||
- Modern, colorful, and professional design
|
||||
- Easy navigation through different research tools
|
||||
- Responsive layout that works on various screen sizes
|
||||
|
||||
### 2. Integrated Research Tools
|
||||
- **Search Engine Research**: Google SERP Search and Tavily AI Search
|
||||
- **Neural Search**: Metaphor Neural Search for finding conceptually similar content
|
||||
- **Trend Analysis**: Google Trends Researcher for analyzing search term popularity
|
||||
- **Web Crawling & Analysis**: Tools for extracting and analyzing web content
|
||||
|
||||
### 3. Guided Research Workflows
|
||||
- Comprehensive Topic Research workflow
|
||||
- Content Gap Analysis workflow
|
||||
- Content Refresh & Update workflow
|
||||
|
||||
## Installation
|
||||
|
||||
1. Ensure you have Python 3.8+ installed
|
||||
2. Install the required dependencies:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Set up the necessary API keys in your environment variables or .env file
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to the dashboard directory:
|
||||
```
|
||||
cd AI-Writer/lib/alwrity_ui/alwrity_researcher
|
||||
```
|
||||
|
||||
2. Run the dashboard:
|
||||
```
|
||||
python -m streamlit run main.py
|
||||
```
|
||||
|
||||
3. Access the dashboard in your web browser at http://localhost:8501
|
||||
|
||||
## Dashboard Sections
|
||||
|
||||
### Dashboard Home
|
||||
Provides an overview of available research tools and featured workflows.
|
||||
|
||||
### Search Tools
|
||||
Access to Google SERP Search and Tavily AI Search for comprehensive search capabilities.
|
||||
|
||||
### Neural Search
|
||||
Use Metaphor Neural Search to find content based on conceptual similarity rather than keyword matching.
|
||||
|
||||
### Trend Analysis
|
||||
Analyze search term popularity over time using Google Trends Researcher.
|
||||
|
||||
### Web Crawling
|
||||
Extract structured content from websites using various web crawling tools.
|
||||
|
||||
### Research Workflows
|
||||
Guided workflows that combine multiple tools for specific research objectives.
|
||||
|
||||
## Current Implementation Status
|
||||
|
||||
This is the initial implementation of the dashboard with placeholder functionality. The UI is fully implemented, but the actual integration with the AI web research modules will be completed in the next phase.
|
||||
|
||||
### What's Implemented
|
||||
- Complete user interface with all sections and forms
|
||||
- Mock data visualization for demonstration purposes
|
||||
- Placeholder functionality for all research tools
|
||||
|
||||
### Next Steps
|
||||
- Integrate with actual AI web research modules
|
||||
- Implement data processing and visualization with real data
|
||||
- Add user authentication and result saving functionality
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Built with Streamlit for rapid UI development
|
||||
- Modular design for easy extension and maintenance
|
||||
- Responsive layout that works on desktop and mobile devices
|
||||
|
||||
## File Structure
|
||||
|
||||
- `main.py`: Entry point for the application
|
||||
- `dashboard.py`: Main dashboard implementation
|
||||
- `utils.py`: Utility functions for data processing and visualization
|
||||
- `style.css`: Custom CSS for styling the dashboard
|
||||
- `requirements.txt`: Required Python packages
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
66
ToBeMigrated/alwrity_ui/alwrity_researcher/config.py
Normal file
66
ToBeMigrated/alwrity_ui/alwrity_researcher/config.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import streamlit as st
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
def get_api_key(key_name):
|
||||
"""Get API key from environment variables or Streamlit secrets"""
|
||||
# Try to get from environment variables first
|
||||
api_key = os.getenv(key_name)
|
||||
|
||||
# If not found in environment, try Streamlit secrets
|
||||
if not api_key and key_name in st.secrets:
|
||||
api_key = st.secrets[key_name]
|
||||
|
||||
return api_key
|
||||
|
||||
def check_api_configuration():
|
||||
"""Check if all required API keys are configured"""
|
||||
api_status = {
|
||||
"SERPER_API_KEY": bool(get_api_key("SERPER_API_KEY")),
|
||||
"TAVILY_API_KEY": bool(get_api_key("TAVILY_API_KEY")),
|
||||
"METAPHOR_API_KEY": bool(get_api_key("METAPHOR_API_KEY")),
|
||||
"FIRECRAWL_API_KEY": bool(get_api_key("FIRECRAWL_API_KEY"))
|
||||
}
|
||||
|
||||
return api_status
|
||||
|
||||
def display_api_configuration_status():
|
||||
"""Display API configuration status in the sidebar with improved styling"""
|
||||
api_status = check_api_configuration()
|
||||
|
||||
st.sidebar.markdown("<div class='api-status-container'>", unsafe_allow_html=True)
|
||||
st.sidebar.markdown("<div class='api-status-title'>API Configuration Status</div>", unsafe_allow_html=True)
|
||||
|
||||
# Display API status with improved styling
|
||||
for api_name, is_configured in api_status.items():
|
||||
if is_configured:
|
||||
st.sidebar.markdown(
|
||||
f"<div class='api-status-item configured'>✅ {api_name}</div>",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
else:
|
||||
st.sidebar.markdown(
|
||||
f"<div class='api-status-item not-configured'>❌ {api_name}</div>",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
|
||||
# Display instructions if any API key is missing with improved styling
|
||||
if not all(api_status.values()):
|
||||
with st.sidebar.expander("How to configure API keys"):
|
||||
st.markdown("""
|
||||
<div style="background-color: #f8fafc; padding: 12px; border-radius: 6px; border-left: 4px solid #3b82f6;">
|
||||
<p style="margin-bottom: 10px; font-weight: 500;">To configure missing API keys, create a <code>.env</code> file in the project root with the following format:</p>
|
||||
<pre style="background-color: #f1f5f9; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 0.9em;">
|
||||
SERPER_API_KEY=your_serper_api_key
|
||||
TAVILY_API_KEY=your_tavily_api_key
|
||||
METAPHOR_API_KEY=your_metaphor_api_key
|
||||
FIRECRAWL_API_KEY=your_firecrawl_api_key
|
||||
</pre>
|
||||
<p style="margin-top: 10px;">Alternatively, you can set these as environment variables in your system.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.sidebar.markdown("</div>", unsafe_allow_html=True)
|
||||
729
ToBeMigrated/alwrity_ui/alwrity_researcher/dashboard.py
Normal file
729
ToBeMigrated/alwrity_ui/alwrity_researcher/dashboard.py
Normal file
@@ -0,0 +1,729 @@
|
||||
import streamlit as st
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import modules
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
|
||||
# Import utils module
|
||||
from utils import (
|
||||
load_css,
|
||||
display_google_serp_results,
|
||||
display_tavily_results,
|
||||
display_metaphor_results,
|
||||
display_google_trends_results,
|
||||
display_crawler_results,
|
||||
display_analyzer_results
|
||||
)
|
||||
|
||||
# Configure Streamlit page settings
|
||||
st.set_page_config(
|
||||
page_title="AI Web Researcher Dashboard",
|
||||
page_icon="🔍",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
# Apply custom CSS immediately
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main-header {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.category-header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 15px 0;
|
||||
color: #0066cc;
|
||||
}
|
||||
/* Make the dashboard responsive */
|
||||
.stTabs [data-baseweb="tab-list"] {
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
white-space: pre-wrap;
|
||||
min-width: fit-content;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
/* Adjust container width */
|
||||
.block-container {
|
||||
max-width: 95% !important;
|
||||
padding: 1rem 1rem 10rem !important;
|
||||
}
|
||||
/* Additional styling for better visibility */
|
||||
.stApp {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.stTabs [data-baseweb="tab"] [data-testid="stMarkdownContainer"] p {
|
||||
font-size: 14px !important;
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
.stTabs [data-baseweb="tab-list"] button {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
}
|
||||
.stTabs [data-baseweb="tab-list"] button[aria-selected="true"] {
|
||||
background-color: #e7f1ff;
|
||||
border-color: #b8daff;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# CSS is now loaded at the top of the file
|
||||
|
||||
# Initialize session state variables
|
||||
def init_session_state():
|
||||
if 'current_section' not in st.session_state:
|
||||
st.session_state['current_section'] = 'Dashboard Home'
|
||||
|
||||
# Initialize session state at startup
|
||||
init_session_state()
|
||||
|
||||
def main():
|
||||
# Initialize session state before accessing it
|
||||
init_session_state()
|
||||
|
||||
# Main navigation header
|
||||
st.markdown('<div class="main-nav-header">AI Web Researcher</div>', unsafe_allow_html=True)
|
||||
|
||||
# Create tabs for navigation
|
||||
selected_section = st.tabs([
|
||||
"🏠 Dashboard Home",
|
||||
"🔍 Search Tools",
|
||||
"🧠 Neural Search",
|
||||
"📈 Trend Analysis",
|
||||
"🕸️ Web Crawling",
|
||||
"📚 Academic Research",
|
||||
"📋 Research Workflows"
|
||||
])
|
||||
|
||||
# Display appropriate section based on selected tab
|
||||
with selected_section[0]:
|
||||
display_home()
|
||||
with selected_section[1]:
|
||||
display_search_tools()
|
||||
with selected_section[2]:
|
||||
display_neural_search()
|
||||
with selected_section[3]:
|
||||
display_trend_analysis()
|
||||
with selected_section[4]:
|
||||
display_web_crawling()
|
||||
with selected_section[5]:
|
||||
display_academic_research()
|
||||
with selected_section[6]:
|
||||
display_research_workflows()
|
||||
|
||||
# Ensure CSS is consistently applied
|
||||
load_css()
|
||||
|
||||
def display_home():
|
||||
|
||||
# Main header with improved styling
|
||||
st.markdown('<div class="main-header">AI Web Researcher Dashboard</div>', unsafe_allow_html=True)
|
||||
|
||||
# Introduction with better formatting
|
||||
st.markdown("""
|
||||
<div class="intro-container">
|
||||
<p class="intro-text">Welcome to the <span class="intro-highlight">AI Web Researcher Dashboard</span>, a comprehensive suite of research tools designed
|
||||
specifically for content creators and digital marketing professionals. This dashboard integrates
|
||||
various web research modules to streamline your content research process, enhance content quality,
|
||||
and improve workflow efficiency.</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Quick access to tool categories with improved header
|
||||
st.markdown('<div class="category-header">Research Tool Categories</div>', unsafe_allow_html=True)
|
||||
|
||||
# Use 2 columns with equal width for better layout
|
||||
col1, col2 = st.columns([1, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">🔍 Search Engine Research</div>
|
||||
<div class="tool-description">Powerful search tools to understand search intent, identify content gaps, and discover high-performing content in your niche</div>
|
||||
<div class="tool-features"><b>Includes:</b> Google SERP Search • Tavily AI Search</div>
|
||||
<div class="tool-features"><b>Use cases:</b> Keyword research, competitor analysis, content planning, identifying user questions</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<span class="tool-badge">SERP Analysis</span>
|
||||
<span class="tool-badge ai-powered">AI-Powered</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">🧠 Neural Search</div>
|
||||
<div class="tool-description">Advanced semantic search technology that understands concepts rather than just keywords to find truly relevant content</div>
|
||||
<div class="tool-features"><b>Includes:</b> Metaphor Neural Search</div>
|
||||
<div class="tool-features"><b>Use cases:</b> Finding conceptually similar articles, discovering unique content angles, competitive research</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<span class="tool-badge semantic">Semantic</span>
|
||||
<span class="tool-badge deep-learning">Deep Learning</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">📈 Trend Analysis</div>
|
||||
<div class="tool-description">Comprehensive trend analysis tools that track search term popularity over time to identify seasonal patterns and emerging topics</div>
|
||||
<div class="tool-features"><b>Includes:</b> Google Trends Researcher</div>
|
||||
<div class="tool-features"><b>Use cases:</b> Seasonal content planning, topic validation, identifying rising trends, content calendar optimization</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<span class="tool-badge time-series">Time Series</span>
|
||||
<span class="tool-badge forecasting">Forecasting</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">🕸️ Web Crawling & Analysis</div>
|
||||
<div class="tool-description">Powerful web extraction tools that gather and structure content from websites for in-depth analysis and insights</div>
|
||||
<div class="tool-features"><b>Includes:</b> Async Web Crawler • Firecrawl Web Crawler • Website Analyzer</div>
|
||||
<div class="tool-features"><b>Use cases:</b> Content auditing, competitor analysis, data extraction, market research, content aggregation</div>
|
||||
<div style="margin-top: 12px;">
|
||||
<span class="tool-badge content-extraction">Content Extraction</span>
|
||||
<span class="tool-badge data-analysis">Data Analysis</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Featured workflow with improved header
|
||||
st.markdown('<div class="category-header">Featured Research Workflow</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="workflow-card">
|
||||
<div class="workflow-title">Comprehensive Topic Research Workflow</div>
|
||||
<p style="color: #4b5563; margin-bottom: 1.25rem;">Follow this proven research workflow to develop comprehensive, data-driven content that resonates with your audience and performs well in search.</p>
|
||||
<div class="workflow-step-container">
|
||||
<div class="workflow-step-number">1</div>
|
||||
<div class="workflow-step-content">
|
||||
<div class="step-title">Initial Exploration</div>
|
||||
<div class="workflow-step-description">Use Google SERP Search to understand the search landscape, identify user intent, and discover what content currently ranks well. Analyze People Also Ask questions to identify key user concerns.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-step-container">
|
||||
<div class="workflow-step-number">2</div>
|
||||
<div class="workflow-step-content">
|
||||
<div class="step-title">In-depth Research</div>
|
||||
<div class="workflow-step-description">Use Tavily AI Search for deeper research on identified subtopics. The AI-powered search provides more contextual information and helps uncover expert insights that might be missed in traditional searches.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-step-container">
|
||||
<div class="workflow-step-number">3</div>
|
||||
<div class="workflow-step-content">
|
||||
<div class="step-title">Competitive Analysis</div>
|
||||
<div class="workflow-step-description">Use Metaphor Neural Search to find conceptually similar content from competitors. Analyze their approach, identify content gaps, and discover unique angles that differentiate your content.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-step-container">
|
||||
<div class="workflow-step-number">4</div>
|
||||
<div class="workflow-step-content">
|
||||
<div class="step-title">Trend Validation</div>
|
||||
<div class="workflow-step-description">Use Google Trends Researcher to verify topic popularity, identify seasonal patterns, and discover related trending topics. This ensures your content is timely and aligned with current audience interests.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-step-container">
|
||||
<div class="workflow-step-number">5</div>
|
||||
<div class="workflow-step-content">
|
||||
<div class="step-title">Content Extraction</div>
|
||||
<div class="workflow-step-description">Use Web Crawling tools to extract specific content from top-performing pages for detailed analysis. This helps identify content structure, depth, and formatting approaches that resonate with your audience.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def display_search_tools():
|
||||
|
||||
st.markdown('<div class="main-header">Search Engine Research Tools</div>', unsafe_allow_html=True)
|
||||
|
||||
# Google SERP Search
|
||||
st.markdown('<div class="category-header">Google SERP Search</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Google SERP Search</div>
|
||||
<div class="tool-description">Retrieves organic search results, People Also Ask questions, and related searches</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> Initial topic research, understanding search intent, and identifying content gaps<br>
|
||||
<b>Key features:</b> Comprehensive search results with structured data extraction
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Google SERP Search
|
||||
with st.form("google_serp_search_form"):
|
||||
search_query = st.text_input("Enter your search query")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
num_results = st.slider("Number of results", 5, 30, 10)
|
||||
with col2:
|
||||
include_paa = st.checkbox("Include People Also Ask", value=True)
|
||||
|
||||
submitted = st.form_submit_button("Search")
|
||||
if submitted and search_query:
|
||||
try:
|
||||
with st.spinner("Searching Google SERP..."):
|
||||
# Import the actual module
|
||||
from ai_web_researcher.google_serp_search import google_search
|
||||
|
||||
# Call the actual implementation
|
||||
results = google_search(search_query)
|
||||
|
||||
# Display the results
|
||||
if results:
|
||||
display_google_serp_results(results)
|
||||
else:
|
||||
st.error("No results found. Please try a different query.")
|
||||
except Exception as e:
|
||||
st.error(f"Error performing Google SERP search: {str(e)}")
|
||||
st.info("Please check your API configuration in the .env file.")
|
||||
st.info("Required API: SERPER_API_KEY for Google SERP Search")
|
||||
display_google_serp_results(None)
|
||||
|
||||
# Tavily AI Search
|
||||
st.markdown('<div class="category-header">Tavily AI Search</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Tavily AI Search</div>
|
||||
<div class="tool-description">Advanced AI-powered search with semantic understanding</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> In-depth research requiring contextual understanding<br>
|
||||
<b>Key features:</b> Provides direct answers to questions and follow-up question suggestions
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Tavily AI Search
|
||||
with st.form("tavily_ai_search_form"):
|
||||
search_query = st.text_input("Enter your search query or question")
|
||||
search_depth = st.select_slider("Search depth", options=["basic", "medium", "deep"], value="medium")
|
||||
|
||||
submitted = st.form_submit_button("Search with Tavily AI")
|
||||
if submitted and search_query:
|
||||
try:
|
||||
with st.spinner("Searching with Tavily AI..."):
|
||||
# Import the actual module
|
||||
from ai_web_researcher.tavily_ai_search import do_tavily_ai_search
|
||||
|
||||
# Call the actual implementation
|
||||
results = do_tavily_ai_search(search_query, search_depth=search_depth)
|
||||
|
||||
# Display the results
|
||||
if results:
|
||||
display_tavily_results(results)
|
||||
else:
|
||||
st.error("No results found. Please try a different query.")
|
||||
except Exception as e:
|
||||
st.error(f"Error performing Tavily AI search: {str(e)}")
|
||||
st.info("Please check your API configuration in the .env file.")
|
||||
st.info("Required API: TAVILY_API_KEY for Tavily AI Search")
|
||||
display_tavily_results(None)
|
||||
|
||||
def display_neural_search():
|
||||
|
||||
st.markdown('<div class="main-header">Neural Search Tools</div>', unsafe_allow_html=True)
|
||||
|
||||
# Metaphor Neural Search
|
||||
st.markdown('<div class="category-header">Metaphor Neural Search</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Metaphor Neural Search</div>
|
||||
<div class="tool-description">Semantic search technology for finding related content</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> Discovering content based on conceptual similarity rather than keyword matching<br>
|
||||
<b>Key features:</b> Find similar articles, competitor analysis, and content inspiration
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Metaphor Neural Search
|
||||
with st.form("metaphor_search_form"):
|
||||
st.markdown("**Search by keyword or find similar content to a URL**")
|
||||
search_type = st.radio("Search type", ["Keyword Search", "Similar Content Search"])
|
||||
|
||||
if search_type == "Keyword Search":
|
||||
search_query = st.text_input("Enter your search query")
|
||||
url_input = ""
|
||||
else:
|
||||
search_query = ""
|
||||
url_input = st.text_input("Enter a URL to find similar content")
|
||||
|
||||
num_results = st.slider("Number of results", 3, 20, 5)
|
||||
|
||||
submitted = st.form_submit_button("Search with Metaphor")
|
||||
if submitted and (search_query or url_input):
|
||||
try:
|
||||
with st.spinner("Searching with Metaphor Neural Search..."):
|
||||
# Import the actual module
|
||||
from ai_web_researcher.metaphor_basic_neural_web_search import metaphor_search_articles, metaphor_find_similar
|
||||
|
||||
# Call the actual implementation
|
||||
if search_query:
|
||||
results = metaphor_search_articles(search_query, num_results=num_results)
|
||||
else:
|
||||
results = metaphor_find_similar(url_input, num_results=num_results)
|
||||
|
||||
# Display the results
|
||||
if results:
|
||||
display_metaphor_results(results)
|
||||
else:
|
||||
st.error("No results found. Please try a different query.")
|
||||
except Exception as e:
|
||||
st.error(f"Error performing Metaphor Neural search: {str(e)}")
|
||||
st.info("Please check your API configuration in the .env file.")
|
||||
st.info("Required API: METAPHOR_API_KEY for Metaphor Neural Search")
|
||||
display_metaphor_results(None)
|
||||
|
||||
def display_trend_analysis():
|
||||
|
||||
st.markdown('<div class="main-header">Trend Analysis Tools</div>', unsafe_allow_html=True)
|
||||
|
||||
# Google Trends Researcher
|
||||
st.markdown('<div class="category-header">Google Trends Researcher</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Google Trends Researcher</div>
|
||||
<div class="tool-description">Analyze search term popularity and related queries</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> Content planning, trend forecasting, and seasonal content optimization<br>
|
||||
<b>Key features:</b> Interest over time charts, related queries, and regional interest data
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Google Trends Researcher
|
||||
with st.form("google_trends_form"):
|
||||
search_terms = st.text_area("Enter search terms (one per line)")
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
time_frame = st.select_slider("Time range", options=["past_hour", "past_day", "past_week", "past_month", "past_90_days", "past_12_months", "past_5_years"], value="past_12_months")
|
||||
with col2:
|
||||
geo = st.text_input("Geographic region (ISO country code, e.g., 'US')", value="US")
|
||||
|
||||
submitted = st.form_submit_button("Analyze Trends")
|
||||
if submitted and search_terms:
|
||||
try:
|
||||
with st.spinner("Analyzing Google Trends..."):
|
||||
# Import the actual module
|
||||
from ai_web_researcher.google_trends_researcher import do_google_trends_analysis
|
||||
|
||||
# Call the actual implementation
|
||||
search_terms_list = [term.strip() for term in search_terms.split('\n') if term.strip()]
|
||||
results = do_google_trends_analysis(search_terms_list, time_frame=time_frame, geo=geo)
|
||||
|
||||
# Display the results
|
||||
if results:
|
||||
display_google_trends_results(results)
|
||||
else:
|
||||
st.error("No trend data found. Please try different search terms.")
|
||||
except Exception as e:
|
||||
st.error(f"Error analyzing Google Trends: {str(e)}")
|
||||
st.info("Google Trends analysis doesn't require an API key, but there might be rate limiting or network issues.")
|
||||
display_google_trends_results(None)
|
||||
|
||||
def display_web_crawling():
|
||||
|
||||
st.markdown('<div class="main-header">Web Crawling & Analysis Tools</div>', unsafe_allow_html=True)
|
||||
|
||||
# Create tabs for different crawling tools
|
||||
tab1, tab2, tab3 = st.tabs(["Firecrawl Web Crawler", "Website Analyzer", "Async Web Crawler"])
|
||||
|
||||
with tab1:
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Firecrawl Web Crawler</div>
|
||||
<div class="tool-description">Extract structured content from websites</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> Content extraction, competitor analysis, and website auditing<br>
|
||||
<b>Key features:</b> Extracts titles, descriptions, headings, and content from web pages
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Firecrawl Web Crawler
|
||||
with st.form("firecrawl_form"):
|
||||
website_url = st.text_input("Enter website URL")
|
||||
depth = st.slider("Crawl depth", 1, 5, 1)
|
||||
max_pages = st.slider("Maximum pages", 1, 50, 10)
|
||||
|
||||
submitted = st.form_submit_button("Crawl Website")
|
||||
if submitted and website_url:
|
||||
try:
|
||||
with st.spinner("Crawling website..."):
|
||||
# Import the actual module
|
||||
from ai_web_researcher.firecrawl_web_crawler import scrape_website
|
||||
|
||||
# Call the actual implementation
|
||||
results = scrape_website(website_url, depth=depth, max_pages=max_pages)
|
||||
|
||||
# Display the results
|
||||
if results:
|
||||
display_crawler_results(results)
|
||||
else:
|
||||
st.error("No crawler results found. Please try a different URL.")
|
||||
except Exception as e:
|
||||
st.error(f"Error crawling website: {str(e)}")
|
||||
st.info("Please check your API configuration in the .env file.")
|
||||
st.info("Required API: FIRECRAWL_API_KEY for Web Crawler")
|
||||
display_crawler_results(None)
|
||||
|
||||
with tab2:
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Website Analyzer</div>
|
||||
<div class="tool-description">Analyze website content and structure</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> Content analysis, SEO auditing, and competitor research<br>
|
||||
<b>Key features:</b> Content analysis, keyword extraction, and readability metrics
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Website Analyzer
|
||||
with st.form("website_analyzer_form"):
|
||||
website_url = st.text_input("Enter website URL")
|
||||
analyze_type = st.selectbox("Analysis type", ["Basic Analysis", "SEO Analysis", "Content Analysis", "Competitor Analysis"])
|
||||
|
||||
submitted = st.form_submit_button("Analyze Website")
|
||||
if submitted and website_url:
|
||||
st.info("Website Analyzer is coming soon. This feature is under development.")
|
||||
|
||||
with tab3:
|
||||
st.markdown("""
|
||||
<div class="tool-card">
|
||||
<div class="tool-title">Async Web Crawler</div>
|
||||
<div class="tool-description">High-performance asynchronous web crawler</div>
|
||||
<div class="tool-features">
|
||||
<b>Best for:</b> Large-scale crawling, data extraction, and content aggregation<br>
|
||||
<b>Key features:</b> Fast, efficient crawling with customizable extraction rules
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Async Web Crawler
|
||||
with st.form("async_crawler_form"):
|
||||
website_url = st.text_input("Enter website URL")
|
||||
max_urls = st.slider("Maximum URLs to crawl", 10, 100, 30)
|
||||
|
||||
submitted = st.form_submit_button("Start Crawling")
|
||||
if submitted and website_url:
|
||||
st.info("Async Web Crawler is coming soon. This feature is under development.")
|
||||
|
||||
def display_academic_research():
|
||||
|
||||
st.markdown('<div class="main-header">Academic Research Tools</div>', unsafe_allow_html=True)
|
||||
|
||||
# ArXiv Search Section
|
||||
st.markdown('<div class="category-header">ArXiv Scholarly Search</div>', unsafe_allow_html=True)
|
||||
|
||||
# Search Parameters
|
||||
search_col1, search_col2 = st.columns([2, 1])
|
||||
with search_col1:
|
||||
search_query = st.text_input("🔍 Enter research topic or keywords", key="arxiv_search")
|
||||
with search_col2:
|
||||
max_results = st.number_input("Maximum Results", min_value=1, max_value=50, value=10)
|
||||
|
||||
# Search Button
|
||||
if st.button("🔎 Search ArXiv", key="arxiv_search_button"):
|
||||
if search_query:
|
||||
with st.spinner("Searching ArXiv database..."):
|
||||
try:
|
||||
# Import arxiv search function
|
||||
from ai_web_researcher.arxiv_schlorly_research import fetch_arxiv_data, create_dataframe
|
||||
|
||||
# Fetch results
|
||||
results = fetch_arxiv_data(search_query, max_results)
|
||||
|
||||
if results:
|
||||
# Create DataFrame
|
||||
df = create_dataframe(results, ["Title", "Published Date", "ArXiv ID", "Summary", "PDF URL"])
|
||||
|
||||
# Display results in an expander
|
||||
with st.expander("📚 Search Results", expanded=True):
|
||||
# Display each paper with options to view abstract and download
|
||||
for idx, row in df.iterrows():
|
||||
st.markdown(f"### {row['Title']}")
|
||||
st.markdown(f"*Published: {row['Published Date']}*")
|
||||
|
||||
# Create columns for buttons
|
||||
btn_col1, btn_col2, btn_col3 = st.columns([1, 1, 1])
|
||||
|
||||
with btn_col1:
|
||||
if st.button(f"📄 View Abstract #{idx}"):
|
||||
st.markdown(f"**Abstract:**\n{row['Summary']}")
|
||||
|
||||
with btn_col2:
|
||||
st.markdown(f"[📥 Download PDF]({row['PDF URL']})")
|
||||
if st.button(f"📝 Summarize #{idx}"):
|
||||
with st.spinner("Generating summary..."):
|
||||
try:
|
||||
from ai_web_researcher.gpt_summarize_web_content import summarize_web_content
|
||||
summary = summarize_web_content(row['PDF URL'])
|
||||
if summary:
|
||||
st.markdown("### GPT Summary")
|
||||
st.markdown(summary)
|
||||
# Add export option for the summary
|
||||
st.download_button(
|
||||
label="📥 Export Summary",
|
||||
data=summary,
|
||||
file_name=f"summary_{row['ArXiv ID']}.txt",
|
||||
mime="text/plain"
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Error generating summary: {str(e)}")
|
||||
|
||||
with btn_col3:
|
||||
if st.button(f"🔍 Related Web Content #{idx}"):
|
||||
# Use Google SERP to find related content
|
||||
from ai_web_researcher.google_serp_search import google_search
|
||||
web_results = google_search(row['Title'])
|
||||
if web_results:
|
||||
st.markdown("### Related Web Content")
|
||||
for result in web_results['organic'][:3]:
|
||||
st.markdown(f"- [{result['title']}]({result['link']})\n {result['snippet']}")
|
||||
|
||||
st.markdown("---")
|
||||
else:
|
||||
st.warning("No results found. Try modifying your search terms.")
|
||||
except Exception as e:
|
||||
st.error(f"An error occurred while searching: {str(e)}")
|
||||
else:
|
||||
st.warning("Please enter a search query.")
|
||||
|
||||
# Research Notes Section
|
||||
st.markdown('<div class="category-header">Research Notes</div>', unsafe_allow_html=True)
|
||||
|
||||
# Initialize session state for notes if not exists
|
||||
if 'research_notes' not in st.session_state:
|
||||
st.session_state.research_notes = {}
|
||||
|
||||
notes_col1, notes_col2 = st.columns([2, 1])
|
||||
|
||||
with notes_col1:
|
||||
paper_id = st.text_input("ArXiv ID or Paper Title", key="notes_paper_id")
|
||||
notes_content = st.text_area("Research Notes", height=200, key="notes_content")
|
||||
|
||||
if st.button("Save Notes"):
|
||||
if paper_id:
|
||||
st.session_state.research_notes[paper_id] = notes_content
|
||||
st.success("Notes saved successfully!")
|
||||
else:
|
||||
st.warning("Please enter a paper identifier.")
|
||||
|
||||
with notes_col2:
|
||||
st.markdown("### Saved Notes")
|
||||
for paper_id, notes in st.session_state.research_notes.items():
|
||||
with st.expander(f"📝 {paper_id}"):
|
||||
st.text_area("Saved Notes", value=notes, height=150, key=f"saved_{paper_id}", disabled=True)
|
||||
if st.button("Export Notes", key=f"export_{paper_id}"):
|
||||
notes_export = f"Research Notes for {paper_id}\n\n{notes}"
|
||||
st.download_button(
|
||||
label="📥 Download Notes",
|
||||
data=notes_export,
|
||||
file_name=f"research_notes_{paper_id}.txt",
|
||||
mime="text/plain"
|
||||
)
|
||||
|
||||
# Citation Management Section
|
||||
st.markdown('<div class="category-header">Citation Management</div>', unsafe_allow_html=True)
|
||||
|
||||
# BibTeX Export
|
||||
st.markdown("### Export Citations")
|
||||
arxiv_id = st.text_input("Enter ArXiv ID for citation export")
|
||||
if arxiv_id and st.button("Generate BibTeX"):
|
||||
try:
|
||||
from ai_web_researcher.arxiv_schlorly_research import arxiv_bibtex
|
||||
bibtex = arxiv_bibtex(arxiv_id)
|
||||
if bibtex:
|
||||
st.code(bibtex, language="bibtex")
|
||||
st.download_button(
|
||||
label="📥 Download BibTeX",
|
||||
data=bibtex,
|
||||
file_name=f"citation_{arxiv_id}.bib",
|
||||
mime="text/plain"
|
||||
)
|
||||
except Exception as e:
|
||||
st.error(f"Error generating citation: {str(e)}")
|
||||
|
||||
def display_research_workflows():
|
||||
|
||||
st.markdown('<div class="main-header">Research Workflows</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="workflow-description">
|
||||
Research workflows combine multiple research tools to provide comprehensive insights for specific content creation tasks.
|
||||
Select a workflow to get started.
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create tabs for different workflows
|
||||
tab1, tab2, tab3 = st.tabs(["Topic Research", "Competitor Analysis", "Trend Discovery"])
|
||||
|
||||
with tab1:
|
||||
st.markdown("""
|
||||
<div class="workflow-card">
|
||||
<div class="workflow-title">Comprehensive Topic Research</div>
|
||||
<div class="workflow-description">
|
||||
This workflow helps you thoroughly research a topic for content creation by combining search results,
|
||||
semantic understanding, and trend analysis.
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Topic Research workflow
|
||||
with st.form("topic_research_form"):
|
||||
topic = st.text_input("Enter your topic")
|
||||
include_trends = st.checkbox("Include trend analysis", value=True)
|
||||
include_competitors = st.checkbox("Include competitor analysis", value=True)
|
||||
|
||||
submitted = st.form_submit_button("Start Research Workflow")
|
||||
if submitted and topic:
|
||||
st.info("Research workflows are coming soon. This feature is under development.")
|
||||
|
||||
with tab2:
|
||||
st.markdown("""
|
||||
<div class="workflow-card">
|
||||
<div class="workflow-title">Competitor Content Analysis</div>
|
||||
<div class="workflow-description">
|
||||
This workflow analyzes your competitors' content to identify gaps and opportunities for your own content strategy.
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Competitor Analysis workflow
|
||||
with st.form("competitor_analysis_form"):
|
||||
competitor_urls = st.text_area("Enter competitor URLs (one per line)")
|
||||
topic_focus = st.text_input("Topic focus (optional)")
|
||||
|
||||
submitted = st.form_submit_button("Start Competitor Analysis")
|
||||
if submitted and competitor_urls:
|
||||
st.info("Research workflows are coming soon. This feature is under development.")
|
||||
|
||||
with tab3:
|
||||
st.markdown("""
|
||||
<div class="workflow-card">
|
||||
<div class="workflow-title">Trend Discovery & Content Planning</div>
|
||||
<div class="workflow-description">
|
||||
This workflow identifies trending topics in your niche and helps you plan content around them.
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Input form for Trend Discovery workflow
|
||||
with st.form("trend_discovery_form"):
|
||||
niche = st.text_input("Enter your niche or industry")
|
||||
time_period = st.select_slider("Time period", options=["past_week", "past_month", "past_90_days", "past_12_months"], value="past_month")
|
||||
|
||||
submitted = st.form_submit_button("Discover Trends")
|
||||
if submitted and niche:
|
||||
st.info("Research workflows are coming soon. This feature is under development.")
|
||||
14
ToBeMigrated/alwrity_ui/alwrity_researcher/main.py
Normal file
14
ToBeMigrated/alwrity_ui/alwrity_researcher/main.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import streamlit as st
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add the current directory to the path
|
||||
sys.path.append(str(Path(__file__).parent))
|
||||
|
||||
# Import the dashboard module
|
||||
from dashboard import main
|
||||
|
||||
# Run the dashboard
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
517
ToBeMigrated/alwrity_ui/alwrity_researcher/style.css
Normal file
517
ToBeMigrated/alwrity_ui/alwrity_researcher/style.css
Normal file
@@ -0,0 +1,517 @@
|
||||
/* Main Dashboard Styles */
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
color: #1f2937;
|
||||
background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.main-header {
|
||||
font-size: 2.2rem;
|
||||
background: linear-gradient(120deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(229, 231, 235, 0.5);
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.category-header {
|
||||
font-size: 1.4rem;
|
||||
background: linear-gradient(90deg, #2563eb 0%, #4f46e5 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 4px solid;
|
||||
border-image: linear-gradient(to bottom, #3b82f6, #4f46e5) 1;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Glassomorphic Tool Card Styles */
|
||||
.tool-card {
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 4px solid;
|
||||
border-image: linear-gradient(to bottom, rgba(59, 130, 246, 0.8), rgba(79, 70, 229, 0.8)) 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 30px rgba(31, 38, 135, 0.25);
|
||||
border-image: linear-gradient(to bottom, rgba(59, 130, 246, 1), rgba(79, 70, 229, 1)) 1;
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.9) 0%, rgba(240, 249, 255, 0.8) 100%);
|
||||
}
|
||||
|
||||
.tool-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(90deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tool-description {
|
||||
color: #374151;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.tool-features {
|
||||
font-size: 0.95rem;
|
||||
color: #4b5563;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.6;
|
||||
background-color: rgba(243, 244, 246, 0.6);
|
||||
padding: 0.85rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
/* Tool Badge Styles */
|
||||
.tool-badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
background-color: rgba(243, 244, 246, 0.8);
|
||||
color: #4b5563;
|
||||
border: 1px solid rgba(209, 213, 219, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.tool-badge.ai-powered {
|
||||
background-color: rgba(79, 70, 229, 0.15);
|
||||
color: #4338ca;
|
||||
border-color: rgba(79, 70, 229, 0.3);
|
||||
}
|
||||
|
||||
.tool-badge.semantic {
|
||||
background-color: rgba(16, 185, 129, 0.15);
|
||||
color: #065f46;
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.tool-badge.deep-learning {
|
||||
background-color: rgba(245, 158, 11, 0.15);
|
||||
color: #92400e;
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.tool-badge.time-series {
|
||||
background-color: rgba(6, 182, 212, 0.15);
|
||||
color: #0e7490;
|
||||
border-color: rgba(6, 182, 212, 0.3);
|
||||
}
|
||||
|
||||
.tool-badge.forecasting {
|
||||
background-color: rgba(168, 85, 247, 0.15);
|
||||
color: #6d28d9;
|
||||
border-color: rgba(168, 85, 247, 0.3);
|
||||
}
|
||||
|
||||
.tool-badge.content-extraction {
|
||||
background-color: rgba(236, 72, 153, 0.15);
|
||||
color: #be185d;
|
||||
border-color: rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
.tool-badge.data-analysis {
|
||||
background-color: rgba(14, 165, 233, 0.15);
|
||||
color: #0369a1;
|
||||
border-color: rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
/* Glassomorphic Workflow Card Styles */
|
||||
.workflow-card {
|
||||
background: linear-gradient(135deg, rgba(239, 246, 255, 0.8) 0%, rgba(219, 234, 254, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 4px solid;
|
||||
border-image: linear-gradient(to bottom, rgba(37, 99, 235, 0.8), rgba(79, 70, 229, 0.8)) 1;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.workflow-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(90deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(191, 219, 254, 0.5);
|
||||
}
|
||||
|
||||
.workflow-step-container {
|
||||
display: flex;
|
||||
margin-bottom: 1.25rem;
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.7) 0%, rgba(240, 249, 255, 0.6) 100%);
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 4px 6px rgba(31, 38, 135, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.workflow-step-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
background: linear-gradient(135deg, rgba(37, 99, 235, 0.9) 0%, rgba(79, 70, 229, 0.9) 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
margin-right: 1rem;
|
||||
box-shadow: 0 4px 6px rgba(37, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
.workflow-step-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
background: linear-gradient(90deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.workflow-step-description {
|
||||
color: #4b5563;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Navigation Styles */
|
||||
.nav-header {
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(120deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(229, 231, 235, 0.5);
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-left: 4px solid;
|
||||
border-image: linear-gradient(to bottom, rgba(59, 130, 246, 0.8), rgba(79, 70, 229, 0.8)) 1;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 30px rgba(31, 38, 135, 0.25);
|
||||
border-image: linear-gradient(to bottom, rgba(59, 130, 246, 1), rgba(79, 70, 229, 1)) 1;
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.9) 0%, rgba(240, 249, 255, 0.8) 100%);
|
||||
}
|
||||
|
||||
.nav-button.selected {
|
||||
background: linear-gradient(120deg, rgba(239, 246, 255, 0.9) 0%, rgba(219, 234, 254, 0.8) 100%);
|
||||
border-image: linear-gradient(to bottom, #3b82f6, #4f46e5) 1;
|
||||
box-shadow: 0 8px 20px rgba(31, 38, 135, 0.2);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: linear-gradient(90deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(90deg, #1e3a8a 0%, #3b82f6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-testid="stSidebar"] {
|
||||
background-color: rgba(248, 250, 252, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-right: 1px solid rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
[data-testid="stSidebarNav"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.st-emotion-cache-16txtl3 {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
margin: 1rem 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(229, 231, 235, 0.5);
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.stButton>button {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #4f46e5 100%);
|
||||
color: white;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
box-shadow: 0 4px 6px rgba(31, 38, 135, 0.15);
|
||||
}
|
||||
|
||||
.stButton>button:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #4338ca 100%);
|
||||
box-shadow: 0 8px 15px rgba(31, 38, 135, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Glassomorphic Results Display Styles */
|
||||
.results-container {
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 1.75rem;
|
||||
margin-top: 1.25rem;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid rgba(229, 231, 235, 0.5);
|
||||
margin-bottom: 1.25rem;
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.7) 0%, rgba(248, 250, 252, 0.6) 100%);
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 4px 6px rgba(31, 38, 135, 0.1);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: #1e3a8a;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.result-snippet {
|
||||
color: #4b5563;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.result-url {
|
||||
color: #059669;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.6rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.stTextInput>div>div>input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(209, 213, 219, 0.5);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.stTextInput>div>div>input:focus {
|
||||
border-color: rgba(59, 130, 246, 0.7);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
/* Dashboard Home Styles */
|
||||
.intro-container {
|
||||
background: linear-gradient(135deg, rgba(239, 246, 255, 0.8) 0%, rgba(224, 242, 254, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 1.75rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 4px solid rgba(59, 130, 246, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
color: #374151;
|
||||
line-height: 1.7;
|
||||
font-size: 1.05rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.intro-highlight {
|
||||
color: #1e3a8a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Chart Container Styles */
|
||||
.chart-container {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin: 1.25rem 0;
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1e3a8a;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Tab Styles */
|
||||
.stTabs [data-baseweb="tab-list"] {
|
||||
gap: 12px;
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
}
|
||||
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.7) 0%, rgba(240, 249, 255, 0.6) 100%);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-left: 3px solid;
|
||||
border-image: linear-gradient(to bottom, rgba(59, 130, 246, 0.8), rgba(79, 70, 229, 0.8)) 1;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
|
||||
.stTabs [data-baseweb="tab"]:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 15px rgba(31, 38, 135, 0.15);
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
}
|
||||
|
||||
.stTabs [aria-selected="true"] {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #4f46e5 100%) !important;
|
||||
color: white !important;
|
||||
border-image: none !important;
|
||||
border-left: 3px solid #3b82f6 !important;
|
||||
box-shadow: 0 8px 15px rgba(59, 130, 246, 0.25) !important;
|
||||
}
|
||||
|
||||
/* Additional Styles for Tabs Content */
|
||||
.stTabs [data-baseweb="tab-panel"] {
|
||||
background: linear-gradient(120deg, rgba(255, 255, 255, 0.8) 0%, rgba(240, 249, 255, 0.7) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 0 12px 12px 12px;
|
||||
padding: 1.75rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.15);
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.tool-card, .workflow-card, .intro-container, .results-container {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.tool-title, .workflow-title {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
}
|
||||
380
ToBeMigrated/alwrity_ui/alwrity_researcher/utils.py
Normal file
380
ToBeMigrated/alwrity_ui/alwrity_researcher/utils.py
Normal file
@@ -0,0 +1,380 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import modules
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||
|
||||
# Import research modules (placeholder imports for now)
|
||||
try:
|
||||
from ai_web_researcher import (
|
||||
google_serp_search,
|
||||
tavily_ai_search,
|
||||
metaphor_basic_neural_web_search,
|
||||
google_trends_researcher,
|
||||
firecrawl_web_crawler
|
||||
)
|
||||
except ImportError:
|
||||
# For development/testing without actual modules
|
||||
pass
|
||||
|
||||
def load_css():
|
||||
"""Load custom CSS"""
|
||||
css_file = Path(__file__).parent / "style.css"
|
||||
with open(css_file) as f:
|
||||
css_content = f.read()
|
||||
# Use session state to track if CSS has been loaded
|
||||
if 'css_loaded' not in st.session_state:
|
||||
st.session_state['css_loaded'] = False
|
||||
|
||||
# Always apply CSS on each page load to ensure styles persist during navigation
|
||||
st.markdown(f"<style>{css_content}</style>", unsafe_allow_html=True)
|
||||
st.session_state['css_loaded'] = True
|
||||
|
||||
def display_google_serp_results(results):
|
||||
"""Display Google SERP search results"""
|
||||
# Check if results are available
|
||||
if not results:
|
||||
st.warning("No search results available. Please try a different query or check your API configuration.")
|
||||
return
|
||||
|
||||
st.markdown('<div class="results-container">', unsafe_allow_html=True)
|
||||
|
||||
# Display organic results
|
||||
st.markdown('<div class="category-header">Search Results</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display actual organic results
|
||||
organic_results = results.get('organic', [])
|
||||
if organic_results:
|
||||
for result in organic_results:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{result.get('title', 'No Title')}</div>
|
||||
<div class="result-url">{result.get('link', '#')}</div>
|
||||
<div class="result-snippet">{result.get('snippet', 'No description available.')}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("No organic search results found.")
|
||||
|
||||
# Display People Also Ask
|
||||
paa_results = results.get('peopleAlsoAsk', [])
|
||||
if paa_results:
|
||||
st.markdown('<div class="category-header">People Also Ask</div>', unsafe_allow_html=True)
|
||||
|
||||
for question in paa_results:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{question.get('question', 'No Question')}</div>
|
||||
<div class="result-snippet">{question.get('snippet', 'No answer available.')}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display Related Searches if available
|
||||
related_searches = results.get('relatedSearches', [])
|
||||
if related_searches:
|
||||
st.markdown('<div class="category-header">Related Searches</div>', unsafe_allow_html=True)
|
||||
|
||||
for search in related_searches:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{search}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_tavily_results(results):
|
||||
"""Display Tavily AI search results"""
|
||||
# Check if results are available
|
||||
if not results:
|
||||
st.warning("No Tavily search results available. Please try a different query or check your API configuration.")
|
||||
return
|
||||
|
||||
st.markdown('<div class="results-container">', unsafe_allow_html=True)
|
||||
|
||||
# Display answer if available
|
||||
answer = results.get('answer', '')
|
||||
if answer:
|
||||
st.markdown('<div class="category-header">Answer</div>', unsafe_allow_html=True)
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-snippet">{answer}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display search results
|
||||
search_results = results.get('results', [])
|
||||
if search_results:
|
||||
st.markdown('<div class="category-header">Search Results</div>', unsafe_allow_html=True)
|
||||
|
||||
for result in search_results:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{result.get('title', 'No Title')}</div>
|
||||
<div class="result-url">{result.get('url', '#')}</div>
|
||||
<div class="result-snippet">{result.get('content', 'No content available.')}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("No search results found.")
|
||||
|
||||
# Display follow-up questions if available
|
||||
follow_up_questions = results.get('follow_up_questions', [])
|
||||
if follow_up_questions:
|
||||
st.markdown('<div class="category-header">Follow-up Questions</div>', unsafe_allow_html=True)
|
||||
|
||||
for question in follow_up_questions:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{question}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_metaphor_results(results):
|
||||
"""Display Metaphor Neural Search results"""
|
||||
# Check if results are available
|
||||
if not results:
|
||||
st.warning("No Metaphor search results available. Please try a different query or check your API configuration.")
|
||||
return
|
||||
|
||||
st.markdown('<div class="results-container">', unsafe_allow_html=True)
|
||||
|
||||
# Display search results
|
||||
st.markdown('<div class="category-header">Similar Content</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display actual results
|
||||
documents = results.get('documents', [])
|
||||
if documents:
|
||||
for doc in documents:
|
||||
title = doc.get('title', 'No Title')
|
||||
url = doc.get('url', '#')
|
||||
extract = doc.get('extract', 'No content available.')
|
||||
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{title}</div>
|
||||
<div class="result-url">{url}</div>
|
||||
<div class="result-snippet">{extract}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("No similar content found.")
|
||||
|
||||
# Display summary if available
|
||||
summary = results.get('summary', '')
|
||||
if summary:
|
||||
st.markdown('<div class="category-header">Content Summary</div>', unsafe_allow_html=True)
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-snippet">{summary}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_google_trends_results(results):
|
||||
"""Display Google Trends results"""
|
||||
# Check if results are available
|
||||
if not results:
|
||||
st.warning("No Google Trends results available. Please try a different query or check your API configuration.")
|
||||
return
|
||||
|
||||
st.markdown('<div class="results-container">', unsafe_allow_html=True)
|
||||
|
||||
# Display interest over time chart if available
|
||||
interest_over_time = results.get('interest_over_time')
|
||||
if interest_over_time is not None and not interest_over_time.empty:
|
||||
st.markdown('<div class="category-header">Interest Over Time</div>', unsafe_allow_html=True)
|
||||
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
||||
st.markdown('<div class="chart-title">Search Interest Over Time</div>', unsafe_allow_html=True)
|
||||
|
||||
# Convert to DataFrame if it's not already
|
||||
if not isinstance(interest_over_time, pd.DataFrame):
|
||||
interest_over_time = pd.DataFrame(interest_over_time)
|
||||
|
||||
# Prepare data for visualization
|
||||
if 'date' in interest_over_time.columns:
|
||||
# Melt the DataFrame to get it in the right format for plotting
|
||||
terms = [col for col in interest_over_time.columns if col != 'date']
|
||||
df = interest_over_time.melt('date', value_vars=terms, var_name='Term', value_name='Interest')
|
||||
|
||||
# Create and display the chart
|
||||
fig = px.line(df, x='date', y='Interest', color='Term', title='Search Interest Over Time')
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
else:
|
||||
st.error("Interest over time data is not in the expected format.")
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display related queries if available
|
||||
related_queries = results.get('related_queries', {})
|
||||
if related_queries:
|
||||
st.markdown('<div class="category-header">Related Queries</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display top related queries
|
||||
for term, queries in related_queries.items():
|
||||
if 'top' in queries:
|
||||
st.markdown(f'<div class="subcategory-header">Top queries for "{term}"</div>', unsafe_allow_html=True)
|
||||
for query in queries['top'].get('query', []):
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{query}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if 'rising' in queries:
|
||||
st.markdown(f'<div class="subcategory-header">Rising queries for "{term}"</div>', unsafe_allow_html=True)
|
||||
for query in queries['rising'].get('query', []):
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{query}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display related topics if available
|
||||
related_topics = results.get('related_topics', {})
|
||||
if related_topics:
|
||||
st.markdown('<div class="category-header">Related Topics</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display top related topics
|
||||
for term, topics in related_topics.items():
|
||||
if 'top' in topics:
|
||||
st.markdown(f'<div class="subcategory-header">Top topics for "{term}"</div>', unsafe_allow_html=True)
|
||||
for topic in topics['top'].get('topic', []):
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{topic}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_crawler_results(results):
|
||||
"""Display Web Crawler results"""
|
||||
# Check if results are available
|
||||
if not results:
|
||||
st.warning("No web crawler results available. Please try a different URL or check your API configuration.")
|
||||
return
|
||||
|
||||
st.markdown('<div class="results-container">', unsafe_allow_html=True)
|
||||
|
||||
# Display crawled pages
|
||||
st.markdown('<div class="category-header">Crawled Pages</div>', unsafe_allow_html=True)
|
||||
|
||||
# Handle different result formats
|
||||
if isinstance(results, dict):
|
||||
# Single page result
|
||||
page_data = results
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{page_data.get('title', 'No Title')}</div>
|
||||
<div class="result-url">{page_data.get('url', '#')}</div>
|
||||
<div class="result-snippet">
|
||||
<b>Title:</b> {page_data.get('title', 'No Title')}<br>
|
||||
<b>Description:</b> {page_data.get('description', 'No description available.')}<br>
|
||||
<b>Word Count:</b> {page_data.get('word_count', 'Unknown')}
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display content sections if available
|
||||
content = page_data.get('content', [])
|
||||
if content:
|
||||
st.markdown('<div class="category-header">Page Content</div>', unsafe_allow_html=True)
|
||||
for section in content:
|
||||
if isinstance(section, dict):
|
||||
section_type = section.get('type', '')
|
||||
section_content = section.get('content', '')
|
||||
if section_type and section_content:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{section_type}</div>
|
||||
<div class="result-snippet">{section_content}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
elif isinstance(results, list):
|
||||
# Multiple pages result
|
||||
for page_data in results:
|
||||
if isinstance(page_data, dict):
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{page_data.get('title', 'No Title')}</div>
|
||||
<div class="result-url">{page_data.get('url', '#')}</div>
|
||||
<div class="result-snippet">
|
||||
<b>Title:</b> {page_data.get('title', 'No Title')}<br>
|
||||
<b>Description:</b> {page_data.get('description', 'No description available.')}<br>
|
||||
<b>Word Count:</b> {page_data.get('word_count', 'Unknown')}
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display content structure
|
||||
st.markdown('<div class="category-header">Content Structure</div>', unsafe_allow_html=True)
|
||||
|
||||
# Placeholder data for chart
|
||||
labels = ['Blog Posts', 'Product Pages', 'Category Pages', 'About Pages', 'Contact Pages']
|
||||
values = [38, 27, 18, 10, 7]
|
||||
|
||||
fig = go.Figure(data=[go.Pie(labels=labels, values=values, hole=.3)])
|
||||
fig.update_layout(title_text='Content Type Distribution')
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
def display_analyzer_results(results):
|
||||
"""Display Website Analyzer results"""
|
||||
# This is a placeholder function that will be implemented when integrated with actual modules
|
||||
st.markdown('<div class="results-container">', unsafe_allow_html=True)
|
||||
|
||||
# Display content quality metrics
|
||||
st.markdown('<div class="category-header">Content Quality Metrics</div>', unsafe_allow_html=True)
|
||||
|
||||
# Placeholder data for chart
|
||||
categories = ['Readability', 'Engagement', 'Relevance', 'Uniqueness', 'Comprehensiveness']
|
||||
values = [4.2, 3.8, 4.5, 3.9, 4.1]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatterpolar(
|
||||
r=values,
|
||||
theta=categories,
|
||||
fill='toself',
|
||||
name='Content Quality'
|
||||
))
|
||||
fig.update_layout(
|
||||
polar=dict(
|
||||
radialaxis=dict(
|
||||
visible=True,
|
||||
range=[0, 5]
|
||||
)),
|
||||
showlegend=False
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Display SEO recommendations
|
||||
st.markdown('<div class="category-header">SEO Recommendations</div>', unsafe_allow_html=True)
|
||||
|
||||
# Placeholder data
|
||||
recommendations = [
|
||||
"Improve meta descriptions for better click-through rates",
|
||||
"Add more internal links to related content",
|
||||
"Optimize images with descriptive alt text",
|
||||
"Improve page loading speed by optimizing images",
|
||||
"Add structured data markup for better search visibility"
|
||||
]
|
||||
|
||||
for recommendation in recommendations:
|
||||
st.markdown(f"""
|
||||
<div class="result-item">
|
||||
<div class="result-title">{recommendation}</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
684
ToBeMigrated/alwrity_ui/content_performance_predictor_ui.py
Normal file
684
ToBeMigrated/alwrity_ui/content_performance_predictor_ui.py
Normal file
@@ -0,0 +1,684 @@
|
||||
"""
|
||||
Streamlit UI for Content Performance Predictor
|
||||
Interactive interface for predicting and optimizing content performance
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import asyncio
|
||||
import json
|
||||
import plotly.graph_objects as go
|
||||
import plotly.express as px
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
|
||||
# Import the predictor
|
||||
try:
|
||||
from lib.content_performance_predictor.content_performance_predictor import (
|
||||
ContentPerformancePredictor,
|
||||
ContentType,
|
||||
predict_content_performance
|
||||
)
|
||||
except ImportError:
|
||||
st.error("Content Performance Predictor module not found. Please check the installation.")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentPerformancePredictorUI:
|
||||
"""Streamlit UI for Content Performance Predictor"""
|
||||
|
||||
def __init__(self):
|
||||
self.predictor = None
|
||||
self.initialize_predictor()
|
||||
|
||||
def initialize_predictor(self):
|
||||
"""Initialize the predictor with error handling"""
|
||||
try:
|
||||
self.predictor = ContentPerformancePredictor()
|
||||
except Exception as e:
|
||||
st.error(f"Failed to initialize predictor: {str(e)}")
|
||||
self.predictor = None
|
||||
|
||||
def render_main_interface(self):
|
||||
"""Render the main Content Performance Predictor interface"""
|
||||
st.title("🎯 Content Performance Predictor")
|
||||
st.markdown("### Predict content success before you publish!")
|
||||
|
||||
# Create tabs for different features
|
||||
tab1, tab2, tab3, tab4 = st.tabs([
|
||||
"📊 Predict Performance",
|
||||
"📈 Batch Analysis",
|
||||
"🔧 Optimization Tools",
|
||||
"📚 Performance History"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
self.render_single_prediction_tab()
|
||||
|
||||
with tab2:
|
||||
self.render_batch_analysis_tab()
|
||||
|
||||
with tab3:
|
||||
self.render_optimization_tab()
|
||||
|
||||
with tab4:
|
||||
self.render_history_tab()
|
||||
|
||||
def render_single_prediction_tab(self):
|
||||
"""Render single content prediction interface"""
|
||||
st.header("Single Content Analysis")
|
||||
|
||||
# Input section
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
# Content input
|
||||
content_input = st.text_area(
|
||||
"Enter your content:",
|
||||
height=200,
|
||||
placeholder="Paste your content here...\n\nExample:\n🚀 Just discovered an amazing AI writing tool that's changing the game!\n\n#AIWriting #ContentCreation"
|
||||
)
|
||||
|
||||
# Target keywords
|
||||
keywords_input = st.text_input(
|
||||
"Target Keywords (optional):",
|
||||
placeholder="AI writing, content creation, SEO"
|
||||
)
|
||||
|
||||
with col2:
|
||||
# Content type selection
|
||||
content_type = st.selectbox(
|
||||
"Content Type:",
|
||||
["twitter", "linkedin", "facebook", "instagram", "blog_post", "email", "youtube"],
|
||||
help="Select the platform where you plan to publish"
|
||||
)
|
||||
|
||||
# Analysis options
|
||||
st.subheader("Analysis Options")
|
||||
|
||||
include_seo = st.checkbox("Include SEO Analysis", value=True)
|
||||
include_trends = st.checkbox("Include Trend Analysis", value=True)
|
||||
include_competitor = st.checkbox("Include Competitor Analysis", value=False)
|
||||
|
||||
# Advanced settings
|
||||
with st.expander("Advanced Settings"):
|
||||
target_audience = st.selectbox(
|
||||
"Target Audience:",
|
||||
["General", "Business", "Tech", "Marketing", "Education", "Entertainment"]
|
||||
)
|
||||
|
||||
urgency_level = st.slider(
|
||||
"Content Urgency:",
|
||||
0.0, 1.0, 0.5,
|
||||
help="How time-sensitive is this content?"
|
||||
)
|
||||
|
||||
# Predict button
|
||||
if st.button("🎯 Predict Performance", type="primary", use_container_width=True):
|
||||
if not content_input.strip():
|
||||
st.error("Please enter content to analyze")
|
||||
return
|
||||
|
||||
# Process keywords
|
||||
keywords = [k.strip() for k in keywords_input.split(",")] if keywords_input else None
|
||||
|
||||
# Show loading spinner
|
||||
with st.spinner("Analyzing content performance..."):
|
||||
# Run prediction
|
||||
prediction_result = self.run_prediction(
|
||||
content_input,
|
||||
content_type,
|
||||
keywords,
|
||||
include_seo,
|
||||
include_trends,
|
||||
include_competitor
|
||||
)
|
||||
|
||||
if prediction_result:
|
||||
self.display_prediction_results(prediction_result)
|
||||
else:
|
||||
st.error("Failed to analyze content. Please try again.")
|
||||
|
||||
def run_prediction(
|
||||
self,
|
||||
content: str,
|
||||
content_type: str,
|
||||
keywords: Optional[List[str]],
|
||||
include_seo: bool,
|
||||
include_trends: bool,
|
||||
include_competitor: bool
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Run the content performance prediction"""
|
||||
try:
|
||||
# Run async prediction
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
result = loop.run_until_complete(
|
||||
predict_content_performance(
|
||||
content=content,
|
||||
content_type=content_type,
|
||||
target_keywords=keywords
|
||||
)
|
||||
)
|
||||
|
||||
loop.close()
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in prediction: {str(e)}")
|
||||
st.error(f"Prediction failed: {str(e)}")
|
||||
return None
|
||||
|
||||
def display_prediction_results(self, result: Dict[str, Any]):
|
||||
"""Display the prediction results with visualizations"""
|
||||
st.success("✅ Analysis Complete!")
|
||||
|
||||
# Overall score section
|
||||
st.subheader("📊 Overall Performance Score")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
# Overall score gauge
|
||||
score = result.get("overall_score", 0)
|
||||
self.render_score_gauge(score, "Overall Score")
|
||||
|
||||
with col2:
|
||||
# Success probability
|
||||
prob = result.get("success_probability", 0) * 100
|
||||
self.render_score_gauge(prob, "Success Probability")
|
||||
|
||||
with col3:
|
||||
# Performance rating
|
||||
rating = self.get_performance_rating(score)
|
||||
st.metric(
|
||||
"Performance Rating",
|
||||
rating["label"],
|
||||
help=rating["description"]
|
||||
)
|
||||
|
||||
# Detailed scores breakdown
|
||||
st.subheader("🔍 Detailed Score Breakdown")
|
||||
scores = result.get("individual_scores", {})
|
||||
if scores:
|
||||
self.render_scores_breakdown(scores)
|
||||
|
||||
# Recommendations section
|
||||
st.subheader("💡 Optimization Recommendations")
|
||||
recommendations = result.get("recommendations", [])
|
||||
if recommendations:
|
||||
for i, rec in enumerate(recommendations, 1):
|
||||
st.markdown(f"**{i}.** {rec}")
|
||||
else:
|
||||
st.info("No specific recommendations available")
|
||||
|
||||
# Predicted metrics
|
||||
st.subheader("📊 Predicted Performance Metrics")
|
||||
metrics = result.get("predicted_metrics", {})
|
||||
if metrics:
|
||||
self.render_predicted_metrics(metrics)
|
||||
|
||||
# Content analysis details
|
||||
st.subheader("📝 Content Analysis Details")
|
||||
analysis = result.get("content_analysis", {})
|
||||
if analysis:
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Word Count", analysis.get("word_count", 0))
|
||||
with col2:
|
||||
st.metric("Character Count", analysis.get("character_count", 0))
|
||||
with col3:
|
||||
st.metric("Hashtags", analysis.get("hashtag_count", 0))
|
||||
with col4:
|
||||
st.metric("Readability", f"{analysis.get('readability_score', 0):.1f}")
|
||||
|
||||
# Export options
|
||||
st.subheader("📤 Export Results")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
if st.button("📄 Generate Report"):
|
||||
report = self.generate_text_report(result)
|
||||
st.text_area("Report:", value=report, height=200)
|
||||
|
||||
with col2:
|
||||
if st.button("📊 Download JSON"):
|
||||
json_str = json.dumps(result, indent=2)
|
||||
st.download_button(
|
||||
label="Download JSON",
|
||||
data=json_str,
|
||||
file_name=f"content_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
||||
mime="application/json"
|
||||
)
|
||||
|
||||
def render_score_gauge(self, score: float, title: str):
|
||||
"""Render a gauge chart for scores"""
|
||||
fig = go.Figure(go.Indicator(
|
||||
mode = "gauge+number+delta",
|
||||
value = score,
|
||||
domain = {'x': [0, 1], 'y': [0, 1]},
|
||||
title = {'text': title},
|
||||
delta = {'reference': 50},
|
||||
gauge = {
|
||||
'axis': {'range': [None, 100]},
|
||||
'bar': {'color': "darkblue"},
|
||||
'steps': [
|
||||
{'range': [0, 25], 'color': "lightgray"},
|
||||
{'range': [25, 50], 'color': "yellow"},
|
||||
{'range': [50, 75], 'color': "orange"},
|
||||
{'range': [75, 100], 'color': "green"}],
|
||||
'threshold': {
|
||||
'line': {'color': "red", 'width': 4},
|
||||
'thickness': 0.75,
|
||||
'value': 90}}))
|
||||
|
||||
fig.update_layout(height=300)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
def render_scores_breakdown(self, scores: Dict[str, float]):
|
||||
"""Render radar chart for score breakdown"""
|
||||
categories = list(scores.keys())
|
||||
values = list(scores.values())
|
||||
|
||||
# Close the radar chart
|
||||
categories.append(categories[0])
|
||||
values.append(values[0])
|
||||
|
||||
fig = go.Figure()
|
||||
|
||||
fig.add_trace(go.Scatterpolar(
|
||||
r=values,
|
||||
theta=categories,
|
||||
fill='toself',
|
||||
name='Performance Scores'
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
polar=dict(
|
||||
radialaxis=dict(
|
||||
visible=True,
|
||||
range=[0, 100]
|
||||
)),
|
||||
showlegend=True,
|
||||
title="Performance Score Breakdown"
|
||||
)
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Display scores as metrics
|
||||
cols = st.columns(len(scores))
|
||||
for i, (category, score) in enumerate(scores.items()):
|
||||
with cols[i]:
|
||||
st.metric(category.title(), f"{score:.1f}")
|
||||
|
||||
def render_predicted_metrics(self, metrics: Dict[str, Any]):
|
||||
"""Render predicted performance metrics"""
|
||||
cols = st.columns(len(metrics))
|
||||
|
||||
for i, (metric, value) in enumerate(metrics.items()):
|
||||
with cols[i]:
|
||||
# Format metric name
|
||||
display_name = metric.replace("predicted_", "").replace("_", " ").title()
|
||||
st.metric(display_name, f"{value:,}")
|
||||
|
||||
def get_performance_rating(self, score: float) -> Dict[str, str]:
|
||||
"""Get performance rating based on score"""
|
||||
if score >= 80:
|
||||
return {"label": "Excellent", "description": "High chance of success"}
|
||||
elif score >= 60:
|
||||
return {"label": "Good", "description": "Solid performance expected"}
|
||||
elif score >= 40:
|
||||
return {"label": "Average", "description": "Room for improvement"}
|
||||
else:
|
||||
return {"label": "Needs Work", "description": "Optimization recommended"}
|
||||
|
||||
def render_batch_analysis_tab(self):
|
||||
"""Render batch analysis interface"""
|
||||
st.header("Batch Content Analysis")
|
||||
st.info("Analyze multiple pieces of content at once")
|
||||
|
||||
# File upload
|
||||
uploaded_file = st.file_uploader(
|
||||
"Upload CSV with content",
|
||||
type=['csv'],
|
||||
help="CSV should have columns: 'content', 'content_type', 'keywords' (optional)"
|
||||
)
|
||||
|
||||
if uploaded_file is not None:
|
||||
try:
|
||||
df = pd.read_csv(uploaded_file)
|
||||
|
||||
# Validate required columns
|
||||
required_cols = ['content', 'content_type']
|
||||
missing_cols = [col for col in required_cols if col not in df.columns]
|
||||
|
||||
if missing_cols:
|
||||
st.error(f"Missing required columns: {', '.join(missing_cols)}")
|
||||
return
|
||||
|
||||
st.success(f"✅ Loaded {len(df)} content items")
|
||||
|
||||
# Preview data
|
||||
with st.expander("Preview Data"):
|
||||
st.dataframe(df.head())
|
||||
|
||||
# Analysis options
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
max_items = st.number_input(
|
||||
"Max items to analyze:",
|
||||
min_value=1,
|
||||
max_value=100,
|
||||
value=min(10, len(df))
|
||||
)
|
||||
|
||||
with col2:
|
||||
export_format = st.selectbox(
|
||||
"Export format:",
|
||||
["CSV", "JSON", "Excel"]
|
||||
)
|
||||
|
||||
# Run batch analysis
|
||||
if st.button("🚀 Run Batch Analysis", type="primary"):
|
||||
with st.spinner(f"Analyzing {max_items} content items..."):
|
||||
batch_df = df.head(max_items)
|
||||
results = self.run_batch_analysis(batch_df)
|
||||
|
||||
if results:
|
||||
self.display_batch_results(results)
|
||||
else:
|
||||
st.error("Batch analysis failed")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error processing file: {str(e)}")
|
||||
|
||||
def run_batch_analysis(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
|
||||
"""Run batch analysis on multiple content items"""
|
||||
results = []
|
||||
progress = st.progress(0)
|
||||
|
||||
for i, row in df.iterrows():
|
||||
try:
|
||||
content = row['content']
|
||||
content_type = row['content_type']
|
||||
keywords = row.get('keywords', '').split(',') if row.get('keywords') else None
|
||||
|
||||
result = self.run_prediction(content, content_type, keywords, True, True, False)
|
||||
if result:
|
||||
result['original_content'] = content
|
||||
result['content_type'] = content_type
|
||||
results.append(result)
|
||||
|
||||
progress.progress((i + 1) / len(df))
|
||||
|
||||
except Exception as e:
|
||||
st.warning(f"Error analyzing row {i}: {str(e)}")
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
def display_batch_results(self, results: List[Dict[str, Any]]):
|
||||
"""Display batch analysis results"""
|
||||
st.success(f"✅ Analyzed {len(results)} content items")
|
||||
|
||||
# Summary statistics
|
||||
st.subheader("📊 Batch Summary")
|
||||
|
||||
scores = [r.get('overall_score', 0) for r in results]
|
||||
avg_score = sum(scores) / len(scores) if scores else 0
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Average Score", f"{avg_score:.1f}")
|
||||
with col2:
|
||||
st.metric("Best Score", f"{max(scores):.1f}" if scores else "0")
|
||||
with col3:
|
||||
st.metric("Worst Score", f"{min(scores):.1f}" if scores else "0")
|
||||
with col4:
|
||||
good_content = len([s for s in scores if s >= 60])
|
||||
st.metric("Good Content", f"{good_content}/{len(scores)}")
|
||||
|
||||
# Results table
|
||||
st.subheader("📋 Detailed Results")
|
||||
|
||||
# Create summary DataFrame
|
||||
summary_data = []
|
||||
for i, result in enumerate(results):
|
||||
summary_data.append({
|
||||
"Content": result['original_content'][:50] + "..." if len(result['original_content']) > 50 else result['original_content'],
|
||||
"Type": result['content_type'],
|
||||
"Overall Score": result.get('overall_score', 0),
|
||||
"Success Probability": result.get('success_probability', 0) * 100,
|
||||
"Engagement": result.get('individual_scores', {}).get('engagement', 0),
|
||||
"SEO": result.get('individual_scores', {}).get('seo', 0),
|
||||
"Virality": result.get('individual_scores', {}).get('virality', 0)
|
||||
})
|
||||
|
||||
summary_df = pd.DataFrame(summary_data)
|
||||
st.dataframe(summary_df, use_container_width=True)
|
||||
|
||||
# Download results
|
||||
if st.button("📥 Download Results"):
|
||||
csv = summary_df.to_csv(index=False)
|
||||
st.download_button(
|
||||
label="Download CSV",
|
||||
data=csv,
|
||||
file_name=f"batch_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
|
||||
mime="text/csv"
|
||||
)
|
||||
|
||||
def render_optimization_tab(self):
|
||||
"""Render content optimization tools"""
|
||||
st.header("🔧 Content Optimization Tools")
|
||||
|
||||
# A/B Testing section
|
||||
st.subheader("🧪 A/B Content Testing")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Version A**")
|
||||
content_a = st.text_area(
|
||||
"Content A:",
|
||||
height=150,
|
||||
key="content_a",
|
||||
placeholder="Enter first version of your content..."
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("**Version B**")
|
||||
content_b = st.text_area(
|
||||
"Content B:",
|
||||
height=150,
|
||||
key="content_b",
|
||||
placeholder="Enter second version of your content..."
|
||||
)
|
||||
|
||||
# Common settings
|
||||
content_type = st.selectbox(
|
||||
"Content Type for both:",
|
||||
["twitter", "linkedin", "facebook", "instagram", "blog_post", "email", "youtube"],
|
||||
key="ab_content_type"
|
||||
)
|
||||
|
||||
if st.button("⚡ Compare Versions", type="primary"):
|
||||
if not content_a.strip() or not content_b.strip():
|
||||
st.error("Please enter both versions of content")
|
||||
return
|
||||
|
||||
with st.spinner("Comparing content versions..."):
|
||||
result_a = self.run_prediction(content_a, content_type, None, True, True, False)
|
||||
result_b = self.run_prediction(content_b, content_type, None, True, True, False)
|
||||
|
||||
if result_a and result_b:
|
||||
self.display_ab_comparison(result_a, result_b)
|
||||
|
||||
# Optimization suggestions
|
||||
st.subheader("💡 Optimization Suggestions")
|
||||
|
||||
optimization_content = st.text_area(
|
||||
"Content to optimize:",
|
||||
height=150,
|
||||
placeholder="Enter content for optimization suggestions..."
|
||||
)
|
||||
|
||||
if st.button("🚀 Get Suggestions") and optimization_content.strip():
|
||||
with st.spinner("Generating optimization suggestions..."):
|
||||
suggestions = self.generate_optimization_suggestions(optimization_content)
|
||||
|
||||
if suggestions:
|
||||
st.success("✅ Optimization suggestions generated!")
|
||||
for i, suggestion in enumerate(suggestions, 1):
|
||||
st.markdown(f"**{i}.** {suggestion}")
|
||||
|
||||
def display_ab_comparison(self, result_a: Dict[str, Any], result_b: Dict[str, Any]):
|
||||
"""Display A/B test comparison results"""
|
||||
st.success("✅ A/B Comparison Complete!")
|
||||
|
||||
# Overall comparison
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
score_a = result_a.get('overall_score', 0)
|
||||
score_b = result_b.get('overall_score', 0)
|
||||
winner = "A" if score_a > score_b else "B" if score_b > score_a else "Tie"
|
||||
|
||||
with col1:
|
||||
st.metric("Version A Score", f"{score_a:.1f}")
|
||||
with col2:
|
||||
st.metric("Version B Score", f"{score_b:.1f}")
|
||||
with col3:
|
||||
st.metric("Winner", winner, delta=f"{abs(score_a - score_b):.1f} point difference")
|
||||
|
||||
# Detailed comparison chart
|
||||
scores_a = result_a.get('individual_scores', {})
|
||||
scores_b = result_b.get('individual_scores', {})
|
||||
|
||||
categories = list(scores_a.keys())
|
||||
values_a = list(scores_a.values())
|
||||
values_b = list(scores_b.values())
|
||||
|
||||
# Create comparison bar chart
|
||||
fig = go.Figure(data=[
|
||||
go.Bar(name='Version A', x=categories, y=values_a),
|
||||
go.Bar(name='Version B', x=categories, y=values_b)
|
||||
])
|
||||
|
||||
fig.update_layout(
|
||||
barmode='group',
|
||||
title="Detailed Score Comparison",
|
||||
yaxis_title="Score",
|
||||
xaxis_title="Category"
|
||||
)
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Recommendations comparison
|
||||
st.subheader("📋 Recommendations Comparison")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("**Version A Recommendations:**")
|
||||
recs_a = result_a.get('recommendations', [])
|
||||
for rec in recs_a[:5]:
|
||||
st.markdown(f"• {rec}")
|
||||
|
||||
with col2:
|
||||
st.markdown("**Version B Recommendations:**")
|
||||
recs_b = result_b.get('recommendations', [])
|
||||
for rec in recs_b[:5]:
|
||||
st.markdown(f"• {rec}")
|
||||
|
||||
def generate_optimization_suggestions(self, content: str) -> List[str]:
|
||||
"""Generate optimization suggestions for content"""
|
||||
suggestions = []
|
||||
|
||||
# Basic content analysis
|
||||
word_count = len(content.split())
|
||||
char_count = len(content)
|
||||
hashtag_count = content.count('#')
|
||||
|
||||
# Length optimization
|
||||
if word_count < 10:
|
||||
suggestions.append("Consider adding more detail to your content for better engagement")
|
||||
elif word_count > 50:
|
||||
suggestions.append("Consider shortening your content for better social media performance")
|
||||
|
||||
# Hashtag optimization
|
||||
if hashtag_count == 0:
|
||||
suggestions.append("Add relevant hashtags to increase discoverability")
|
||||
elif hashtag_count > 5:
|
||||
suggestions.append("Reduce the number of hashtags for better readability")
|
||||
|
||||
# Engagement optimization
|
||||
if '?' not in content:
|
||||
suggestions.append("Consider adding a question to encourage engagement")
|
||||
|
||||
if '!' not in content and '.' in content:
|
||||
suggestions.append("Add some excitement with exclamation marks")
|
||||
|
||||
# Call to action
|
||||
cta_words = ['click', 'share', 'comment', 'like', 'follow', 'subscribe']
|
||||
has_cta = any(word in content.lower() for word in cta_words)
|
||||
if not has_cta:
|
||||
suggestions.append("Include a clear call-to-action (like, share, comment)")
|
||||
|
||||
# Emoji usage
|
||||
emoji_count = len([char for char in content if ord(char) > 127])
|
||||
if emoji_count == 0:
|
||||
suggestions.append("Consider adding relevant emojis to make content more engaging")
|
||||
|
||||
return suggestions[:5] # Limit to top 5 suggestions
|
||||
|
||||
def render_history_tab(self):
|
||||
"""Render performance history interface"""
|
||||
st.header("📚 Performance History")
|
||||
st.info("Performance history tracking coming soon!")
|
||||
st.markdown("This feature will allow you to:")
|
||||
st.markdown("• Track your content performance over time")
|
||||
st.markdown("• Compare predicted vs actual performance")
|
||||
st.markdown("• Identify your best-performing content patterns")
|
||||
st.markdown("• Generate performance reports")
|
||||
|
||||
def generate_text_report(self, result: Dict[str, Any]) -> str:
|
||||
"""Generate a text report of the analysis"""
|
||||
report = f"""
|
||||
CONTENT PERFORMANCE ANALYSIS REPORT
|
||||
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
OVERALL PERFORMANCE
|
||||
Overall Score: {result.get('overall_score', 0):.1f}/100
|
||||
Success Probability: {result.get('success_probability', 0)*100:.1f}%
|
||||
Performance Rating: {self.get_performance_rating(result.get('overall_score', 0))['label']}
|
||||
|
||||
DETAILED SCORES
|
||||
"""
|
||||
|
||||
scores = result.get('individual_scores', {})
|
||||
for category, score in scores.items():
|
||||
report += f"{category.title()}: {score:.1f}/100\n"
|
||||
|
||||
report += "\nCONTENT ANALYSIS\n"
|
||||
analysis = result.get('content_analysis', {})
|
||||
for key, value in analysis.items():
|
||||
report += f"{key.replace('_', ' ').title()}: {value}\n"
|
||||
|
||||
report += "\nRECOMMENDATIONS\n"
|
||||
recommendations = result.get('recommendations', [])
|
||||
for i, rec in enumerate(recommendations, 1):
|
||||
report += f"{i}. {rec}\n"
|
||||
|
||||
return report
|
||||
|
||||
def render_content_performance_predictor():
|
||||
"""Main function to render the Content Performance Predictor UI"""
|
||||
ui = ContentPerformancePredictorUI()
|
||||
ui.render_main_interface()
|
||||
|
||||
if __name__ == "__main__":
|
||||
render_content_performance_predictor()
|
||||
510
ToBeMigrated/alwrity_ui/dashboard_styles.py
Normal file
510
ToBeMigrated/alwrity_ui/dashboard_styles.py
Normal file
@@ -0,0 +1,510 @@
|
||||
import streamlit as st
|
||||
|
||||
def apply_dashboard_style():
|
||||
"""Apply common glassmorphic styling for all dashboards (AI Writers, SEO Tools, Social Media)."""
|
||||
st.markdown("""
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
/* Global styling with glassmorphic background */
|
||||
.stApp {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.main .block-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 2rem 2.5rem 3rem 2.5rem !important;
|
||||
max-width: 1400px;
|
||||
margin: 2rem auto;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Modern dashboard header with glassmorphic effect */
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding: 3rem 2rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.6), transparent);
|
||||
animation: shimmer 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 3.2em;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
font-size: 1.2em;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Category headers with modern styling */
|
||||
.stMarkdown h2 {
|
||||
color: #ffffff;
|
||||
font-size: 1.8em;
|
||||
font-weight: 600;
|
||||
margin: 2.5rem 0 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 4px solid rgba(255, 255, 255, 0.6);
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Custom category headers */
|
||||
.category-header {
|
||||
margin: 3rem 0 2rem 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category-header h2 {
|
||||
color: #ffffff;
|
||||
font-size: 1.8em;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem 0;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.category-line {
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0.2) 50%, transparent 100%);
|
||||
border-radius: 1px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Cards container with grid layout */
|
||||
.cards-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Card container */
|
||||
.card-container {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Premium cards (works for both writer and tool cards) */
|
||||
.premium-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 20px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(0) scale(1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Style all Streamlit buttons to match the glassmorphic design */
|
||||
.card-container + div[data-testid="stButton"] > button,
|
||||
.stButton > button {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
border-radius: 20px !important;
|
||||
color: #ffffff !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 1rem !important;
|
||||
padding: 1rem 1.5rem !important;
|
||||
min-height: 60px !important;
|
||||
margin: 0 !important;
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Button hover effects */
|
||||
.card-container + div[data-testid="stButton"] > button:hover,
|
||||
.stButton > button:hover {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
backdrop-filter: blur(25px) !important;
|
||||
transform: translateY(-4px) scale(1.02) !important;
|
||||
box-shadow:
|
||||
0 15px 35px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
/* Button focus state */
|
||||
.card-container + div[data-testid="stButton"] > button:focus,
|
||||
.stButton > button:focus {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.5) !important;
|
||||
outline-offset: 2px !important;
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* Button active state */
|
||||
.card-container + div[data-testid="stButton"] > button:active,
|
||||
.stButton > button:active {
|
||||
transform: translateY(-2px) scale(1.01) !important;
|
||||
transition: all 0.1s ease !important;
|
||||
}
|
||||
|
||||
/* Add shine effect to buttons */
|
||||
.card-container + div[data-testid="stButton"] > button::before,
|
||||
.stButton > button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transform: skewX(-20deg);
|
||||
transition: left 0.6s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-container + div[data-testid="stButton"] > button:hover::before,
|
||||
.stButton > button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
/* Card hover effects when hovering over the container */
|
||||
.card-container:hover .premium-card {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(25px);
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow:
|
||||
0 15px 35px rgba(0, 0, 0, 0.25),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-container:hover .card-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-container:hover .card-arrow {
|
||||
transform: translateX(5px);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.card-container:hover .card-shine {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.card-container:hover .card-icon {
|
||||
filter: drop-shadow(0 0 15px rgba(255, 255, 255, 0.5));
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-container:hover .card-category {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Active/pressed state */
|
||||
.card-container:active .premium-card {
|
||||
transform: translateY(-2px) scale(1.01);
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
/* Card glow effect */
|
||||
.card-glow {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
border-radius: 22px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Card content wrapper */
|
||||
.card-content {
|
||||
padding: 2rem 1.8rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Card icon */
|
||||
.card-icon {
|
||||
font-size: 2.8em;
|
||||
margin-bottom: 1rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.3));
|
||||
line-height: 1;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Card title */
|
||||
.card-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 0.8rem;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Card description */
|
||||
.card-description {
|
||||
font-size: 0.95em;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.5rem;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Card footer */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/* Card category badge */
|
||||
.card-category {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Card arrow */
|
||||
.card-arrow {
|
||||
font-size: 1.2em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Card shine effect */
|
||||
.card-shine {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transform: skewX(-20deg);
|
||||
transition: left 0.6s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Category spacer */
|
||||
.category-spacer {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for cards */
|
||||
@media (max-width: 768px) {
|
||||
.cards-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.premium-card {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
margin: 2rem 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.main .block-container {
|
||||
padding: 1.5rem !important;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
padding: 2rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 2.4em;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Card entrance animations */
|
||||
.premium-card {
|
||||
animation: cardSlideIn 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes cardSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Stagger animation delays */
|
||||
.premium-card:nth-child(1) { animation-delay: 0.1s; }
|
||||
.premium-card:nth-child(2) { animation-delay: 0.2s; }
|
||||
.premium-card:nth-child(3) { animation-delay: 0.3s; }
|
||||
.premium-card:nth-child(4) { animation-delay: 0.4s; }
|
||||
.premium-card:nth-child(5) { animation-delay: 0.5s; }
|
||||
.premium-card:nth-child(6) { animation-delay: 0.6s; }
|
||||
|
||||
/* Hide Streamlit elements */
|
||||
.stApp > header {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Page entrance animation */
|
||||
.main .block-container {
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_dashboard_header(title, description):
|
||||
"""Render a standardized dashboard header."""
|
||||
st.markdown(f"""
|
||||
<div class="dashboard-header">
|
||||
<h1>{title}</h1>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_category_header(category_name):
|
||||
"""Render a standardized category header."""
|
||||
st.markdown(f"""
|
||||
<div class="category-header">
|
||||
<h2>{category_name}</h2>
|
||||
<div class="category-line"></div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def render_card(icon, title, description, category, key_suffix="", help_text=""):
|
||||
"""Render a standardized premium card with button."""
|
||||
st.markdown(f"""
|
||||
<div class="card-container">
|
||||
<div class="premium-card">
|
||||
<div class="card-glow"></div>
|
||||
<div class="card-content">
|
||||
<div class="card-icon">{icon}</div>
|
||||
<div class="card-title">{title}</div>
|
||||
<div class="card-description">{description}</div>
|
||||
<div class="card-footer">
|
||||
<span class="card-category">{category}</span>
|
||||
<div class="card-arrow">→</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-shine"></div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Return button for functionality
|
||||
return st.button(
|
||||
f"Select {title}",
|
||||
key=f"card_{key_suffix}",
|
||||
help=help_text or f"Launch {title} - {description}",
|
||||
use_container_width=True
|
||||
)
|
||||
277
ToBeMigrated/alwrity_ui/display_google_serp_results.py
Normal file
277
ToBeMigrated/alwrity_ui/display_google_serp_results.py
Normal file
@@ -0,0 +1,277 @@
|
||||
import streamlit as st
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
# Configure module logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def display_research_results(results: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Display research results in a structured format with tabs.
|
||||
|
||||
Args:
|
||||
results (dict): Processed research results containing summary and data
|
||||
"""
|
||||
if not results:
|
||||
st.warning("No results to display")
|
||||
return
|
||||
|
||||
# Create tabs for different result sections
|
||||
tabs = st.tabs(["📊 Summary", "🔍 Results", "📈 Statistics"])
|
||||
|
||||
with tabs[0]:
|
||||
display_summary_section(results)
|
||||
|
||||
with tabs[1]:
|
||||
if results['source'] == 'gemini':
|
||||
display_gemini_results(results)
|
||||
else:
|
||||
display_serp_results(results)
|
||||
|
||||
with tabs[2]:
|
||||
display_statistics(results)
|
||||
|
||||
def process_research_results(results: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Process and format research results."""
|
||||
logger.info("Processing research results")
|
||||
|
||||
try:
|
||||
if not results:
|
||||
return None
|
||||
|
||||
processed = {
|
||||
'timestamp': str(datetime.now()),
|
||||
'source': results.get('source', 'unknown'),
|
||||
'summary': {},
|
||||
'data': {}
|
||||
}
|
||||
|
||||
if results.get('source') == 'gemini':
|
||||
processed.update(process_gemini_results(results))
|
||||
else:
|
||||
processed.update(process_serp_results(results))
|
||||
|
||||
logger.info("Results processing completed")
|
||||
return processed
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Failed to process results: {err}", exc_info=True)
|
||||
return None
|
||||
|
||||
def process_search_results(search_results: Dict[str, Any], search_type: str = "general") -> Optional[Dict[str, Any]]:
|
||||
"""Process search results and prepare for display."""
|
||||
logger.info(f"Processing {search_type} search results")
|
||||
|
||||
try:
|
||||
if not search_results:
|
||||
return None
|
||||
|
||||
processed = {
|
||||
'organic': process_organic_results(search_results.get('organic', [])),
|
||||
'peopleAlsoAsk': process_paa_results(search_results.get('peopleAlsoAsk', [])),
|
||||
'relatedSearches': process_related_searches(search_results.get('relatedSearches', [])),
|
||||
'metadata': {
|
||||
'timestamp': str(datetime.now()),
|
||||
'type': search_type
|
||||
}
|
||||
}
|
||||
|
||||
return processed
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Error processing search results: {err}", exc_info=True)
|
||||
return None
|
||||
|
||||
# Helper functions for result processing
|
||||
def process_organic_results(results):
|
||||
"""Process organic search results."""
|
||||
return [{
|
||||
'title': result.get('title', 'No Title'),
|
||||
'link': result.get('link', '#'),
|
||||
'snippet': result.get('snippet', 'No snippet available'),
|
||||
'position': result.get('position', 'N/A')
|
||||
} for result in results]
|
||||
|
||||
def process_paa_results(results):
|
||||
"""Process People Also Ask results."""
|
||||
return [{
|
||||
'question': result.get('title', ''),
|
||||
'answer': result.get('snippet', 'No answer available'),
|
||||
'link': result.get('link', '#')
|
||||
} for result in results]
|
||||
|
||||
def process_related_searches(results):
|
||||
"""Process related searches."""
|
||||
return [query.get('query', '') for query in results]
|
||||
|
||||
def process_gemini_results(results: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Process Gemini API research results.
|
||||
|
||||
Args:
|
||||
results (dict): Raw Gemini research results
|
||||
|
||||
Returns:
|
||||
dict: Processed results with summary and data
|
||||
"""
|
||||
gemini_data = results.get('results', {})
|
||||
return {
|
||||
'summary': {
|
||||
'main_findings': gemini_data.get('main_response', ''),
|
||||
'sources': gemini_data.get('grounding_data', []),
|
||||
'processing_time': gemini_data.get('metadata', {}).get('timestamp'),
|
||||
'total_sources': len(gemini_data.get('grounding_data', [])),
|
||||
'model': gemini_data.get('metadata', {}).get('model', 'unknown')
|
||||
},
|
||||
'data': gemini_data
|
||||
}
|
||||
|
||||
def process_serp_results(results: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Process SERP search results.
|
||||
|
||||
Args:
|
||||
results (dict): Raw SERP results
|
||||
|
||||
Returns:
|
||||
dict: Processed results with summary and data
|
||||
"""
|
||||
organic_results = results.get('organic', [])
|
||||
paa_results = results.get('peopleAlsoAsk', [])
|
||||
related_searches = results.get('relatedSearches', [])
|
||||
|
||||
return {
|
||||
'summary': {
|
||||
'total_results': len(organic_results),
|
||||
'sources': [result.get('link') for result in organic_results],
|
||||
'titles': [result.get('title') for result in organic_results],
|
||||
'total_questions': len(paa_results),
|
||||
'total_related': len(related_searches)
|
||||
},
|
||||
'data': {
|
||||
'organic': process_organic_results(organic_results),
|
||||
'peopleAlsoAsk': process_paa_results(paa_results),
|
||||
'relatedSearches': process_related_searches(related_searches)
|
||||
}
|
||||
}
|
||||
|
||||
# Display helper functions
|
||||
def display_summary_section(results):
|
||||
"""Display summary section of results."""
|
||||
st.markdown("### 📋 Research Summary")
|
||||
st.markdown(f"""
|
||||
- **Source**: {results['source'].title()}
|
||||
- **Time**: {results['timestamp']}
|
||||
- **Total Sources**: {len(results.get('summary', {}).get('sources', []))}
|
||||
""")
|
||||
|
||||
def display_gemini_results(results):
|
||||
"""Display Gemini-specific results."""
|
||||
st.markdown("### 🤖 Gemini Research Findings")
|
||||
st.write(results['summary']['main_findings'])
|
||||
|
||||
with st.expander("🌐 Sources and References", expanded=False):
|
||||
st.write(results['data'].get('grounding_data', 'No sources available'))
|
||||
|
||||
def display_serp_results(results):
|
||||
"""Display SERP-specific results."""
|
||||
st.markdown("### 🔍 Search Results")
|
||||
|
||||
for result in results['data'].get('organic', []):
|
||||
with st.expander(f"📄 {result['title']}", expanded=False):
|
||||
st.markdown(f"""
|
||||
**Rank:** {result['position']}
|
||||
|
||||
**Link:** [{result['link']}]({result['link']})
|
||||
|
||||
**Snippet:**
|
||||
{result['snippet']}
|
||||
""")
|
||||
|
||||
def display_statistics(results: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Display statistical information about search results.
|
||||
|
||||
Args:
|
||||
results (dict): Processed research results
|
||||
"""
|
||||
st.markdown("### 📈 Research Statistics")
|
||||
|
||||
# Source-specific metrics
|
||||
if results['source'] == 'gemini':
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.metric(
|
||||
"Sources Analyzed",
|
||||
results.get('summary', {}).get('total_sources', 0)
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Model Used",
|
||||
results.get('summary', {}).get('model', 'Unknown')
|
||||
)
|
||||
|
||||
else: # SERP results
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric(
|
||||
"Organic Results",
|
||||
results.get('summary', {}).get('total_results', 0)
|
||||
)
|
||||
with col2:
|
||||
st.metric(
|
||||
"Related Questions",
|
||||
results.get('summary', {}).get('total_questions', 0)
|
||||
)
|
||||
with col3:
|
||||
st.metric(
|
||||
"Related Searches",
|
||||
results.get('summary', {}).get('total_related', 0)
|
||||
)
|
||||
|
||||
# Common metrics
|
||||
st.markdown("#### 🕒 Timing Information")
|
||||
st.info(f"Research completed at: {results['timestamp']}")
|
||||
|
||||
# Display data quality metrics
|
||||
st.markdown("#### 📊 Data Quality")
|
||||
quality_metrics = calculate_quality_metrics(results)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.progress(quality_metrics['completeness'])
|
||||
st.caption("Data Completeness")
|
||||
with col2:
|
||||
st.progress(quality_metrics['relevance'])
|
||||
st.caption("Estimated Relevance")
|
||||
|
||||
def calculate_quality_metrics(results: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""
|
||||
Calculate quality metrics for the research results.
|
||||
|
||||
Args:
|
||||
results (dict): Processed research results
|
||||
|
||||
Returns:
|
||||
dict: Quality metrics including completeness and relevance scores
|
||||
"""
|
||||
try:
|
||||
if results['source'] == 'gemini':
|
||||
completeness = 1.0 if results['summary']['main_findings'] else 0.0
|
||||
relevance = 0.8 if results['summary']['sources'] else 0.4
|
||||
else:
|
||||
organic_results = results.get('summary', {}).get('total_results', 0)
|
||||
completeness = min(organic_results / 10, 1.0) # Normalize to 0-1
|
||||
has_paa = bool(results.get('summary', {}).get('total_questions', 0))
|
||||
has_related = bool(results.get('summary', {}).get('total_related', 0))
|
||||
relevance = (0.6 + (0.2 if has_paa else 0) + (0.2 if has_related else 0))
|
||||
|
||||
return {
|
||||
'completeness': completeness,
|
||||
'relevance': relevance
|
||||
}
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Error calculating quality metrics: {err}")
|
||||
return {'completeness': 0.0, 'relevance': 0.0}
|
||||
458
ToBeMigrated/alwrity_ui/google_trends_ui.py
Normal file
458
ToBeMigrated/alwrity_ui/google_trends_ui.py
Normal file
@@ -0,0 +1,458 @@
|
||||
"""
|
||||
Module for displaying Google Trends data in the Streamlit UI.
|
||||
|
||||
This module provides functions for visualizing Google Trends data, including:
|
||||
- Interest over time
|
||||
- Regional interest
|
||||
- Related queries
|
||||
- Related topics
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
import logging
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def display_google_trends_data(trends_data, search_keyword):
|
||||
"""
|
||||
Display Google Trends data in a structured format with tabs for different sections.
|
||||
|
||||
Args:
|
||||
trends_data (dict): Dictionary containing Google Trends data
|
||||
search_keyword (str): The search keyword used for the analysis
|
||||
"""
|
||||
if not trends_data:
|
||||
st.warning("No Google Trends data available for this search.")
|
||||
return
|
||||
|
||||
st.subheader(f"Google Trends Analysis for '{search_keyword}'")
|
||||
|
||||
# Add an informative message about Google Trends
|
||||
with st.expander("ℹ️ About Google Trends Data", expanded=False):
|
||||
st.markdown("""
|
||||
**What is Google Trends?**
|
||||
|
||||
Google Trends is a public web facility that shows how often a particular search-term is entered relative to the total search-volume across various regions of the world, and in various languages.
|
||||
|
||||
**What data is shown here?**
|
||||
|
||||
- **Related Keywords**: Terms that are frequently searched together with your keyword
|
||||
- **Interest Over Time**: How interest in your keyword has changed over the past 12 months
|
||||
- **Regional Interest**: Where in the world your keyword is most popular
|
||||
- **Related Queries**: What people search for before and after searching for your keyword
|
||||
- **Related Topics**: Topics that are closely related to your keyword
|
||||
|
||||
**How to interpret the data:**
|
||||
|
||||
- Interest values range from 0 to 100, where 100 is the peak popularity for the term
|
||||
- A value of 50 means the term is half as popular as the peak
|
||||
- A value of 0 means there was not enough data for this term
|
||||
""")
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
||||
"Related Keywords",
|
||||
"Interest Over Time",
|
||||
"Regional Interest",
|
||||
"Related Queries",
|
||||
"Related Topics"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
display_keywords_section(trends_data.get('related_keywords', []))
|
||||
|
||||
with tab2:
|
||||
display_interest_over_time(trends_data.get('interest_over_time', pd.DataFrame()))
|
||||
|
||||
with tab3:
|
||||
display_regional_interest(trends_data.get('regional_interest', pd.DataFrame()))
|
||||
|
||||
with tab4:
|
||||
display_related_queries(trends_data.get('related_queries', pd.DataFrame()))
|
||||
|
||||
with tab5:
|
||||
display_related_topics(trends_data.get('related_topics', pd.DataFrame()))
|
||||
|
||||
# Add a footer with data source information
|
||||
st.markdown("---")
|
||||
st.caption("Data source: Google Trends | Last updated: " + pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
def display_keywords_section(keywords):
|
||||
"""Display related keywords from Google Trends in a table format."""
|
||||
if not keywords:
|
||||
st.info("No related keywords data available.")
|
||||
return
|
||||
|
||||
st.subheader("Related Keywords")
|
||||
st.write("Keywords related to your search:")
|
||||
|
||||
# Add explanation about related keywords
|
||||
with st.expander("ℹ️ About Related Keywords", expanded=False):
|
||||
st.markdown("""
|
||||
**What are Related Keywords?**
|
||||
|
||||
Related keywords are terms that are frequently searched together with your main keyword.
|
||||
These keywords can help you understand what topics are associated with your search term
|
||||
and can be valuable for content planning and SEO strategies.
|
||||
|
||||
**How to use this data:**
|
||||
|
||||
- Use these keywords to expand your content strategy
|
||||
- Identify gaps in your content that you could fill
|
||||
- Understand what your audience is interested in
|
||||
- Improve your SEO by incorporating these terms naturally in your content
|
||||
""")
|
||||
|
||||
# Create a DataFrame for better display
|
||||
df = pd.DataFrame(keywords, columns=['Keyword'])
|
||||
st.dataframe(df, use_container_width=True)
|
||||
|
||||
# Add a note about the number of keywords
|
||||
st.caption(f"Found {len(keywords)} related keywords")
|
||||
|
||||
def display_interest_over_time(interest_df):
|
||||
"""Display a chart showing interest over time for a given search keyword."""
|
||||
if interest_df.empty:
|
||||
st.info("No interest over time data available.")
|
||||
return
|
||||
|
||||
st.subheader("Interest Over Time")
|
||||
|
||||
# Add explanation about interest over time
|
||||
with st.expander("ℹ️ About Interest Over Time", expanded=False):
|
||||
st.markdown("""
|
||||
**What is Interest Over Time?**
|
||||
|
||||
Interest Over Time shows how interest in your search term has changed over the past 12 months.
|
||||
The data is normalized and presented on a scale from 0 to 100, where 100 is the peak popularity
|
||||
for the term, 50 means the term is half as popular, and 0 means there was not enough data.
|
||||
|
||||
**How to interpret this chart:**
|
||||
|
||||
- Look for peaks and valleys to identify trends
|
||||
- Compare with seasonal patterns or events
|
||||
- Identify if interest is growing, declining, or stable
|
||||
- Use this data to time your content releases for maximum impact
|
||||
""")
|
||||
|
||||
try:
|
||||
# Ensure we have the required columns
|
||||
if 'date' not in interest_df.columns:
|
||||
st.error("Interest over time data is missing the 'date' column.")
|
||||
return
|
||||
|
||||
if 'interest' not in interest_df.columns:
|
||||
st.error("Interest over time data is missing the 'interest' column.")
|
||||
return
|
||||
|
||||
# Create the chart
|
||||
fig = px.line(
|
||||
interest_df,
|
||||
x='date',
|
||||
y='interest',
|
||||
title='Interest Over Time',
|
||||
labels={'date': 'Date', 'interest': 'Interest'},
|
||||
line_shape='spline'
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
xaxis_title="Date",
|
||||
yaxis_title="Interest",
|
||||
hovermode='x unified'
|
||||
)
|
||||
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Add summary statistics
|
||||
if not interest_df.empty:
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Average Interest", f"{interest_df['interest'].mean():.1f}")
|
||||
with col2:
|
||||
st.metric("Peak Interest", f"{interest_df['interest'].max():.1f}")
|
||||
with col3:
|
||||
st.metric("Lowest Interest", f"{interest_df['interest'].min():.1f}")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error displaying interest over time chart: {str(e)}")
|
||||
logger.error(f"Error in display_interest_over_time: {e}")
|
||||
|
||||
def display_regional_interest(regional_df):
|
||||
"""Display a chart showing interest by region for the search keyword."""
|
||||
if regional_df.empty:
|
||||
st.info("No regional interest data available.")
|
||||
return
|
||||
|
||||
st.subheader("Regional Interest")
|
||||
|
||||
# Add explanation about regional interest
|
||||
with st.expander("ℹ️ About Regional Interest", expanded=False):
|
||||
st.markdown("""
|
||||
**What is Regional Interest?**
|
||||
|
||||
Regional Interest shows how interest in your search term varies across different countries.
|
||||
The data is normalized and presented on a scale from 0 to 100, where 100 is the peak popularity
|
||||
for the term in that region, 50 means the term is half as popular, and 0 means there was not enough data.
|
||||
|
||||
**How to interpret this map:**
|
||||
|
||||
- Darker colors indicate higher interest in that region
|
||||
- Lighter colors indicate lower interest
|
||||
- Hover over a country to see the exact interest value
|
||||
- Use this data to target your content to specific regions
|
||||
""")
|
||||
|
||||
try:
|
||||
# Ensure we have the required columns
|
||||
if 'country_code' not in regional_df.columns:
|
||||
st.error("Regional interest data is missing the 'country_code' column.")
|
||||
return
|
||||
|
||||
if 'interest' not in regional_df.columns:
|
||||
st.error("Regional interest data is missing the 'interest' column.")
|
||||
return
|
||||
|
||||
# Create the choropleth map
|
||||
fig = go.Figure(data=go.Choropleth(
|
||||
locations=regional_df['country_code'],
|
||||
z=regional_df['interest'],
|
||||
text=regional_df['country_code'], # This will show in the hover text
|
||||
colorscale='Viridis',
|
||||
colorbar_title="Interest Level",
|
||||
zmin=0,
|
||||
zmax=100,
|
||||
marker_line_color='darkgray',
|
||||
marker_line_width=0.5,
|
||||
showscale=True,
|
||||
colorbar=dict(
|
||||
title="Interest Level",
|
||||
tickformat=".0f",
|
||||
tickmode="linear",
|
||||
tick0=0,
|
||||
dtick=20
|
||||
)
|
||||
))
|
||||
|
||||
# Update the layout for better visualization
|
||||
fig.update_layout(
|
||||
title=dict(
|
||||
text='Regional Interest Distribution',
|
||||
x=0.5,
|
||||
xanchor='center'
|
||||
),
|
||||
geo=dict(
|
||||
showframe=False,
|
||||
showcoastlines=True,
|
||||
projection_type='equirectangular',
|
||||
showland=True,
|
||||
landcolor='lightgray',
|
||||
showocean=True,
|
||||
oceancolor='aliceblue',
|
||||
showcountries=True,
|
||||
countrycolor='darkgray'
|
||||
),
|
||||
width=800,
|
||||
height=500,
|
||||
margin=dict(l=0, r=0, t=30, b=0)
|
||||
)
|
||||
|
||||
# Display the map
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# Display top 5 countries with highest interest
|
||||
if not regional_df.empty:
|
||||
st.subheader("Top Regions by Interest")
|
||||
top_regions = regional_df.sort_values('interest', ascending=False).head(5)
|
||||
|
||||
# Create a more visually appealing bar chart for top regions
|
||||
fig_bar = go.Figure(data=[
|
||||
go.Bar(
|
||||
x=top_regions['country_code'],
|
||||
y=top_regions['interest'],
|
||||
text=top_regions['interest'].round(1),
|
||||
textposition='auto',
|
||||
marker_color='rgb(55, 83, 109)'
|
||||
)
|
||||
])
|
||||
|
||||
fig_bar.update_layout(
|
||||
title='Top 5 Regions by Interest Level',
|
||||
xaxis_title='Region',
|
||||
yaxis_title='Interest Level',
|
||||
yaxis_range=[0, 100],
|
||||
showlegend=False
|
||||
)
|
||||
|
||||
st.plotly_chart(fig_bar, use_container_width=True)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error displaying regional interest chart: {str(e)}")
|
||||
logger.error(f"Error in display_regional_interest: {e}")
|
||||
|
||||
def display_related_queries(queries_df):
|
||||
"""Display related queries in a structured format."""
|
||||
if queries_df.empty:
|
||||
st.info("No related queries data available.")
|
||||
return
|
||||
|
||||
st.subheader("Related Queries")
|
||||
|
||||
# Add explanation about related queries
|
||||
with st.expander("ℹ️ About Related Queries", expanded=False):
|
||||
st.markdown("""
|
||||
**What are Related Queries?**
|
||||
|
||||
Related Queries show what people search for before and after searching for your keyword.
|
||||
These queries can help you understand the search intent and context around your keyword.
|
||||
|
||||
**How to interpret this data:**
|
||||
|
||||
- The 'value' column shows the relative interest compared to your main keyword
|
||||
- Higher values indicate stronger association with your keyword
|
||||
- Use these queries to expand your content strategy
|
||||
- Identify what questions your audience is trying to answer
|
||||
""")
|
||||
|
||||
try:
|
||||
# Ensure we have the required columns
|
||||
if 'query' not in queries_df.columns:
|
||||
st.error("Related queries data is missing the 'query' column.")
|
||||
return
|
||||
|
||||
if 'value' not in queries_df.columns:
|
||||
st.error("Related queries data is missing the 'value' column.")
|
||||
return
|
||||
|
||||
# Sort by value in descending order
|
||||
queries_df = queries_df.sort_values('value', ascending=False)
|
||||
|
||||
# Display as a table
|
||||
st.dataframe(queries_df, use_container_width=True)
|
||||
|
||||
# Add a note about the number of queries
|
||||
st.caption(f"Found {len(queries_df)} related queries")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error displaying related queries: {str(e)}")
|
||||
logger.error(f"Error in display_related_queries: {e}")
|
||||
|
||||
def display_trending_searches(trending_df):
|
||||
"""Display trending searches in the UI."""
|
||||
if trending_df.empty:
|
||||
st.info("No trending searches data available.")
|
||||
return
|
||||
|
||||
st.subheader("📊 Trending Searches")
|
||||
|
||||
# Display as numbered list with emojis
|
||||
for idx, search in enumerate(trending_df[0].head(10), 1):
|
||||
st.write(f"{idx}. 🔍 {search}")
|
||||
|
||||
def display_realtime_trends(trends_df):
|
||||
"""Display realtime trending searches in the UI."""
|
||||
if trends_df.empty:
|
||||
st.info("No realtime trends data available.")
|
||||
return
|
||||
|
||||
st.subheader("⚡ Realtime Trends")
|
||||
|
||||
# Create tabs for different categories
|
||||
if not trends_df.empty:
|
||||
# Display top 5 trends with their titles and articles
|
||||
for _, row in trends_df.head(5).iterrows():
|
||||
with st.expander(f"🔥 {row.get('title', 'Trending Topic')}"):
|
||||
st.write(f"**Traffic:** {row.get('traffic', 'N/A')}")
|
||||
if 'articles' in row:
|
||||
st.write("📰 Related Articles:")
|
||||
for article in row['articles'][:3]: # Show top 3 articles
|
||||
st.write(f"- {article['title']}")
|
||||
|
||||
|
||||
|
||||
def display_related_topics(topics_df):
|
||||
"""Display related topics in a structured format."""
|
||||
if topics_df.empty:
|
||||
st.info("No related topics data available.")
|
||||
return
|
||||
|
||||
st.subheader("Related Topics")
|
||||
|
||||
# Add explanation about related topics
|
||||
with st.expander("ℹ️ About Related Topics", expanded=False):
|
||||
st.markdown("""
|
||||
**What are Related Topics?**
|
||||
|
||||
Related Topics show broader topics that are associated with your search term.
|
||||
These topics can help you understand the broader context and themes related to your keyword.
|
||||
|
||||
**How to interpret this data:**
|
||||
|
||||
- The 'value' column shows the relative interest compared to your main keyword
|
||||
- Higher values indicate stronger association with your keyword
|
||||
- Use these topics to understand the broader context of your keyword
|
||||
- Identify themes that might be relevant to your content strategy
|
||||
""")
|
||||
|
||||
try:
|
||||
# Ensure we have the required columns
|
||||
if 'topic' not in topics_df.columns:
|
||||
st.error("Related topics data is missing the 'topic' column.")
|
||||
return
|
||||
|
||||
if 'value' not in topics_df.columns:
|
||||
st.error("Related topics data is missing the 'value' column.")
|
||||
return
|
||||
|
||||
# Sort by value in descending order
|
||||
topics_df = topics_df.sort_values('value', ascending=False)
|
||||
|
||||
# Display as a table
|
||||
st.dataframe(topics_df, use_container_width=True)
|
||||
|
||||
# Add a note about the number of topics
|
||||
st.caption(f"Found {len(topics_df)} related topics")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error displaying related topics: {str(e)}")
|
||||
logger.error(f"Error in display_related_topics: {e}")
|
||||
|
||||
def process_trends_data(trends_data):
|
||||
"""
|
||||
Process and format Google Trends data for display.
|
||||
|
||||
Args:
|
||||
trends_data (dict): Raw Google Trends data
|
||||
|
||||
Returns:
|
||||
dict: Formatted data ready for display
|
||||
"""
|
||||
if not trends_data:
|
||||
return {}
|
||||
|
||||
processed_data = {}
|
||||
|
||||
# Process related keywords
|
||||
if 'related_keywords' in trends_data:
|
||||
processed_data['related_keywords'] = trends_data['related_keywords']
|
||||
|
||||
# Process interest over time
|
||||
if 'interest_over_time' in trends_data and not trends_data['interest_over_time'].empty:
|
||||
processed_data['interest_over_time'] = trends_data['interest_over_time']
|
||||
|
||||
# Process regional interest
|
||||
if 'regional_interest' in trends_data and not trends_data['regional_interest'].empty:
|
||||
processed_data['regional_interest'] = trends_data['regional_interest']
|
||||
|
||||
# Process related queries
|
||||
if 'related_queries' in trends_data and not trends_data['related_queries'].empty:
|
||||
processed_data['related_queries'] = trends_data['related_queries']
|
||||
|
||||
# Process related topics
|
||||
if 'related_topics' in trends_data and not trends_data['related_topics'].empty:
|
||||
processed_data['related_topics'] = trends_data['related_topics']
|
||||
|
||||
return processed_data
|
||||
585
ToBeMigrated/alwrity_ui/keyword_web_researcher.py
Normal file
585
ToBeMigrated/alwrity_ui/keyword_web_researcher.py
Normal file
@@ -0,0 +1,585 @@
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
import streamlit as st
|
||||
from datetime import datetime
|
||||
|
||||
from lib.ai_web_researcher.gpt_online_researcher import gpt_web_researcher
|
||||
from lib.utils.read_main_config_params import read_return_config_section
|
||||
|
||||
# Configure module-level logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Create console handler if it doesn't exist
|
||||
if not logger.handlers:
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
def reload_env_variables():
|
||||
"""Reload environment variables from .env file."""
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(override=True)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to reload environment variables: {str(e)}")
|
||||
return False
|
||||
|
||||
def save_api_key_to_env(key_name, key_value):
|
||||
"""Save API key to .env file."""
|
||||
try:
|
||||
env_path = os.path.join(os.getcwd(), '.env')
|
||||
|
||||
# Read existing .env content
|
||||
existing_content = {}
|
||||
if os.path.exists(env_path):
|
||||
with open(env_path, 'r') as f:
|
||||
for line in f:
|
||||
if '=' in line:
|
||||
key, value = line.strip().split('=', 1)
|
||||
existing_content[key] = value
|
||||
|
||||
# Update or add new key
|
||||
existing_content[key_name] = key_value
|
||||
|
||||
# Write back to .env
|
||||
with open(env_path, 'w') as f:
|
||||
for key, value in existing_content.items():
|
||||
f.write(f"{key}={value}\n")
|
||||
|
||||
# Update environment variable and reload all env vars
|
||||
os.environ[key_name] = key_value
|
||||
if reload_env_variables():
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save API key to .env: {str(e)}")
|
||||
return False
|
||||
|
||||
def validate_api_keys():
|
||||
"""Validate required API keys and return their status."""
|
||||
|
||||
logger.info("Validating API keys")
|
||||
|
||||
# Get API keys
|
||||
api_keys = {
|
||||
'SERPER_API_KEY': os.getenv('SERPER_API_KEY'),
|
||||
'METAPHOR_API_KEY': os.getenv('METAPHOR_API_KEY'),
|
||||
'TAVILY_API_KEY': os.getenv('TAVILY_API_KEY'),
|
||||
'FIRECRAWL_API_KEY': os.getenv('FIRECRAWL_API_KEY')
|
||||
}
|
||||
|
||||
# Test SERPER_API_KEY validity
|
||||
if api_keys['SERPER_API_KEY']:
|
||||
try:
|
||||
# Make a test request
|
||||
import requests
|
||||
test_url = "https://google.serper.dev/search"
|
||||
headers = {
|
||||
'X-API-KEY': api_keys['SERPER_API_KEY'],
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
test_payload = {"q": "test", "gl": "us", "hl": "en", "num": 1}
|
||||
|
||||
response = requests.post(test_url, headers=headers, json=test_payload)
|
||||
api_keys['SERPER_API_KEY_VALID'] = response.status_code == 200
|
||||
|
||||
if not api_keys['SERPER_API_KEY_VALID']:
|
||||
logger.error(f"SERPER_API_KEY validation failed: {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating SERPER_API_KEY: {str(e)}")
|
||||
api_keys['SERPER_API_KEY_VALID'] = False
|
||||
else:
|
||||
api_keys['SERPER_API_KEY_VALID'] = False
|
||||
|
||||
return api_keys
|
||||
|
||||
def do_web_research():
|
||||
"""Main function to perform web research based on user input."""
|
||||
|
||||
# Reset session state variables for this research operation
|
||||
if 'metaphor_results_displayed' in st.session_state:
|
||||
del st.session_state.metaphor_results_displayed
|
||||
|
||||
logger.info("Starting do_web_research function")
|
||||
|
||||
try:
|
||||
# Get API keys without validation
|
||||
api_keys = {
|
||||
'SERPER_API_KEY': os.getenv('SERPER_API_KEY'),
|
||||
'METAPHOR_API_KEY': os.getenv('METAPHOR_API_KEY'),
|
||||
'TAVILY_API_KEY': os.getenv('TAVILY_API_KEY'),
|
||||
'FIRECRAWL_API_KEY': os.getenv('FIRECRAWL_API_KEY')
|
||||
}
|
||||
|
||||
if not api_keys['SERPER_API_KEY']:
|
||||
st.error("""
|
||||
🚫 SERPER_API_KEY is missing. Please configure your API key.
|
||||
""")
|
||||
with st.popover("⚙️ Configure API Keys"):
|
||||
st.markdown("""
|
||||
### API Key Configuration
|
||||
Enter your API keys below to enable research features.
|
||||
""")
|
||||
|
||||
# SERPER API Key
|
||||
serper_col1, serper_col2 = st.columns([3, 1])
|
||||
with serper_col1:
|
||||
serper_key = st.text_input(
|
||||
"Serper API Key",
|
||||
type="password",
|
||||
placeholder="Enter your Serper API key",
|
||||
help="Get your key at https://serper.dev"
|
||||
)
|
||||
test_key = st.checkbox("Test API key before saving", value=False, help="Validate the API key before saving", key="test_serper_key")
|
||||
with serper_col2:
|
||||
if st.button("Save Serper", use_container_width=True):
|
||||
if serper_key:
|
||||
if test_key:
|
||||
# Test the API key
|
||||
try:
|
||||
import requests
|
||||
test_url = "https://google.serper.dev/search"
|
||||
headers = {
|
||||
'X-API-KEY': serper_key,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
test_payload = {"q": "test", "gl": "us", "hl": "en", "num": 1}
|
||||
response = requests.post(test_url, headers=headers, json=test_payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
if save_api_key_to_env('SERPER_API_KEY', serper_key):
|
||||
st.success("✅ Serper API key validated and saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
else:
|
||||
st.error(f"API key validation failed: {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Error validating API key: {str(e)}")
|
||||
else:
|
||||
# Skip validation and save directly
|
||||
if save_api_key_to_env('SERPER_API_KEY', serper_key):
|
||||
st.success("✅ Serper API key saved!")
|
||||
time.sleep(0.5) # Small delay to ensure the key is saved
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
|
||||
# METAPHOR API Key
|
||||
if not api_keys.get('METAPHOR_API_KEY'):
|
||||
metaphor_col1, metaphor_col2 = st.columns([3, 1])
|
||||
with metaphor_col1:
|
||||
metaphor_key = st.text_input(
|
||||
"Metaphor API Key",
|
||||
type="password",
|
||||
placeholder="Enter your Metaphor API key",
|
||||
help="Get your key at https://metaphor.systems"
|
||||
)
|
||||
test_metaphor = st.checkbox("Test API key before saving", value=False, help="Validate the API key before saving", key="test_metaphor_key")
|
||||
with metaphor_col2:
|
||||
if st.button("Save Metaphor", use_container_width=True):
|
||||
if metaphor_key:
|
||||
if test_metaphor:
|
||||
# Test the API key
|
||||
try:
|
||||
import requests
|
||||
test_url = "https://api.metaphor.systems/v1/search"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {metaphor_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
test_payload = {"query": "test", "numResults": 1}
|
||||
response = requests.post(test_url, headers=headers, json=test_payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
if save_api_key_to_env('METAPHOR_API_KEY', metaphor_key):
|
||||
st.success("✅ Metaphor API key validated and saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
else:
|
||||
st.error(f"API key validation failed: {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Error validating API key: {str(e)}")
|
||||
else:
|
||||
# Skip validation and save directly
|
||||
if save_api_key_to_env('METAPHOR_API_KEY', metaphor_key):
|
||||
st.success("✅ Metaphor API key saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
|
||||
# TAVILY API Key
|
||||
if not api_keys.get('TAVILY_API_KEY'):
|
||||
tavily_col1, tavily_col2 = st.columns([3, 1])
|
||||
with tavily_col1:
|
||||
tavily_key = st.text_input(
|
||||
"Tavily API Key",
|
||||
type="password",
|
||||
placeholder="Enter your Tavily API key",
|
||||
help="Get your key at https://tavily.com"
|
||||
)
|
||||
test_tavily = st.checkbox("Test API key before saving", value=False, help="Validate the API key before saving", key="test_tavily_key")
|
||||
with tavily_col2:
|
||||
if st.button("Save Tavily", use_container_width=True):
|
||||
if tavily_key:
|
||||
if test_tavily:
|
||||
# Test the API key
|
||||
try:
|
||||
import requests
|
||||
test_url = "https://api.tavily.com/v1/search"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {tavily_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
test_payload = {"query": "test", "max_results": 1}
|
||||
response = requests.post(test_url, headers=headers, json=test_payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
if save_api_key_to_env('TAVILY_API_KEY', tavily_key):
|
||||
st.success("✅ Tavily API key validated and saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
else:
|
||||
st.error(f"API key validation failed: {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Error validating API key: {str(e)}")
|
||||
else:
|
||||
# Skip validation and save directly
|
||||
if save_api_key_to_env('TAVILY_API_KEY', tavily_key):
|
||||
st.success("✅ Tavily API key saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
|
||||
# FIRECRAWL API Key
|
||||
if not api_keys.get('FIRECRAWL_API_KEY'):
|
||||
firecrawl_col1, firecrawl_col2 = st.columns([3, 1])
|
||||
with firecrawl_col1:
|
||||
firecrawl_key = st.text_input(
|
||||
"Firecrawl API Key",
|
||||
type="password",
|
||||
placeholder="Enter your Firecrawl API key",
|
||||
help="Get your key at https://firecrawl.co"
|
||||
)
|
||||
test_firecrawl = st.checkbox("Test API key before saving", value=False, help="Validate the API key before saving", key="test_firecrawl_key")
|
||||
with firecrawl_col2:
|
||||
if st.button("Save Firecrawl", use_container_width=True):
|
||||
if firecrawl_key:
|
||||
if test_firecrawl:
|
||||
# Test the API key
|
||||
try:
|
||||
import requests
|
||||
test_url = "https://api.firecrawl.co/v1/search"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {firecrawl_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
test_payload = {"query": "test", "limit": 1}
|
||||
response = requests.post(test_url, headers=headers, json=test_payload)
|
||||
|
||||
if response.status_code == 200:
|
||||
if save_api_key_to_env('FIRECRAWL_API_KEY', firecrawl_key):
|
||||
st.success("✅ Firecrawl API key validated and saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
else:
|
||||
st.error(f"API key validation failed: {response.status_code} - {response.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Error validating API key: {str(e)}")
|
||||
else:
|
||||
# Skip validation and save directly
|
||||
if save_api_key_to_env('FIRECRAWL_API_KEY', firecrawl_key):
|
||||
st.success("✅ Firecrawl API key saved!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to save API key")
|
||||
|
||||
st.markdown("""
|
||||
---
|
||||
### Need Help?
|
||||
1. Click the links above to get your API keys
|
||||
2. Enter the keys in the fields above
|
||||
3. Click Save to store them securely
|
||||
4. The app will refresh automatically
|
||||
""")
|
||||
return
|
||||
|
||||
# Initialize session state for research options
|
||||
if "research_options" not in st.session_state:
|
||||
st.session_state.research_options = {
|
||||
"primary_keywords": "",
|
||||
"related_keywords": "",
|
||||
"target_audience": ["General"],
|
||||
"content_type": ["Blog Posts"],
|
||||
"search_depth": 3,
|
||||
"geo_location": "us",
|
||||
"search_language": "en",
|
||||
"num_results": 10,
|
||||
"time_range": "past month",
|
||||
"include_domains": "",
|
||||
"similar_url": "",
|
||||
"search_mode": "google" # Default search mode
|
||||
}
|
||||
|
||||
# Define the research options dialog function
|
||||
@st.dialog("🔍 Research Options", width="large")
|
||||
def show_research_options():
|
||||
tab1, tab2 = st.tabs(["Basic", "Advanced"])
|
||||
|
||||
with tab1:
|
||||
st.session_state.research_options["related_keywords"] = st.text_input(
|
||||
"Related Keywords",
|
||||
value=st.session_state.research_options["related_keywords"],
|
||||
placeholder="Enter related terms...",
|
||||
help="Additional keywords to provide context and expand research"
|
||||
)
|
||||
|
||||
st.session_state.research_options["target_audience"] = st.multiselect(
|
||||
"Target Audience",
|
||||
["General", "Technical", "Business", "Academic", "Youth", "Senior"],
|
||||
default=st.session_state.research_options["target_audience"],
|
||||
help="Select your target audience to focus research"
|
||||
)
|
||||
|
||||
st.session_state.research_options["content_type"] = st.multiselect(
|
||||
"Content Type",
|
||||
["Blog Posts", "Articles", "Social Media", "Whitepapers", "Tutorials", "Videos"],
|
||||
default=st.session_state.research_options["content_type"],
|
||||
help="Select content types to tailor research results"
|
||||
)
|
||||
|
||||
st.session_state.research_options["search_depth"] = st.slider(
|
||||
"Search Depth",
|
||||
min_value=1,
|
||||
max_value=5,
|
||||
value=st.session_state.research_options["search_depth"],
|
||||
help="Higher depth means more comprehensive but slower research"
|
||||
)
|
||||
|
||||
with tab2:
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.session_state.research_options["geo_location"] = st.selectbox(
|
||||
"Geographic Location",
|
||||
options=["us", "in", "uk", "fr", "de", "jp", "custom"],
|
||||
index=["us", "in", "uk", "fr", "de", "jp"].index(st.session_state.research_options["geo_location"]),
|
||||
help="Target specific geographic region for research"
|
||||
)
|
||||
|
||||
st.session_state.research_options["num_results"] = st.number_input(
|
||||
"Number of Results",
|
||||
min_value=1,
|
||||
max_value=100,
|
||||
value=st.session_state.research_options["num_results"],
|
||||
help="Number of results to analyze"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.session_state.research_options["search_language"] = st.selectbox(
|
||||
"Search Language",
|
||||
options=["en", "hi", "fr", "de", "es", "custom"],
|
||||
index=["en", "hi", "fr", "de", "es"].index(st.session_state.research_options["search_language"]),
|
||||
help="Primary language for search results"
|
||||
)
|
||||
|
||||
st.session_state.research_options["time_range"] = st.selectbox(
|
||||
"Time Range",
|
||||
options=["past day", "past week", "past month", "past year", "anytime"],
|
||||
index=["past day", "past week", "past month", "past year", "anytime"].index(st.session_state.research_options["time_range"]),
|
||||
help="Time period for research results"
|
||||
)
|
||||
|
||||
# Add the technical options to the Advanced tab
|
||||
st.markdown("---")
|
||||
st.markdown("### Advanced Search Parameters")
|
||||
|
||||
st.session_state.research_options["include_domains"] = st.text_input(
|
||||
"Include Domains",
|
||||
value=st.session_state.research_options["include_domains"],
|
||||
placeholder="example.com, another.com",
|
||||
help="Specific domains to include in research"
|
||||
)
|
||||
|
||||
st.session_state.research_options["similar_url"] = st.text_input(
|
||||
"Similar URL",
|
||||
value=st.session_state.research_options["similar_url"],
|
||||
placeholder="https://example.com/page",
|
||||
help="Find content similar to this URL"
|
||||
)
|
||||
|
||||
col1, col2 = st.columns([1, 1])
|
||||
with col1:
|
||||
if st.button("Apply", use_container_width=True, type="primary"):
|
||||
st.session_state.show_options_dialog = False
|
||||
st.rerun()
|
||||
with col2:
|
||||
if st.button("Cancel", use_container_width=True):
|
||||
st.session_state.show_options_dialog = False
|
||||
st.rerun()
|
||||
|
||||
# Main interface
|
||||
st.title("ALwrity Web Researcher")
|
||||
|
||||
# Primary search area with help popover
|
||||
with st.popover("ℹ️ Keyword Research Tips"):
|
||||
st.markdown("""
|
||||
### How to Get Better Results
|
||||
1. **Primary Keywords**: Your main topic or focus
|
||||
2. **Related Keywords**: Supporting terms that add context
|
||||
3. **Search Depth**: Higher depth = more comprehensive but slower
|
||||
4. **Target Audience**: Affects content recommendations
|
||||
5. **Content Type**: Influences research focus
|
||||
6. **Search Mode**: Choose between traditional web research(Google), AI-powered search(Tavily and Metaphor) and Deep Researcher
|
||||
""")
|
||||
|
||||
col1, col2 = st.columns([3, 1])
|
||||
with col1:
|
||||
st.session_state.research_options["primary_keywords"] = st.text_input(
|
||||
"Primary Keywords",
|
||||
value=st.session_state.research_options["primary_keywords"],
|
||||
placeholder="Enter main keywords for research...",
|
||||
help="Enter your main topic or focus keywords"
|
||||
)
|
||||
with col2:
|
||||
if st.button("Research Options", use_container_width=True):
|
||||
show_research_options()
|
||||
|
||||
# Research method selection in main container
|
||||
st.markdown("### Select Research Method")
|
||||
search_options = [
|
||||
("google", "🔍 Google Search", "Traditional web research with AI analysis", bool(api_keys['SERPER_API_KEY'])),
|
||||
("ai", "🤖 AI Search", "Neural search with semantic analysis", bool(api_keys['METAPHOR_API_KEY'] and api_keys['TAVILY_API_KEY'])),
|
||||
("deep", "🔬 Deep Search (Beta)", "Advanced deep web analysis", bool(all(api_keys.values())))
|
||||
]
|
||||
|
||||
enabled_options = [opt[1] for opt in search_options if opt[3]]
|
||||
if enabled_options:
|
||||
selected_option = st.radio(
|
||||
"Search Method",
|
||||
options=enabled_options,
|
||||
horizontal=True,
|
||||
help="Choose your preferred research method"
|
||||
)
|
||||
|
||||
# Map the selected option to the search_mode value
|
||||
for mode, label, _, _ in search_options:
|
||||
if label == selected_option:
|
||||
st.session_state.research_options["search_mode"] = mode
|
||||
break
|
||||
else:
|
||||
st.warning("No search methods available. Please configure API keys.")
|
||||
|
||||
# Execute search button
|
||||
if st.button("🔍 Start Research", type="primary", use_container_width=True):
|
||||
if not st.session_state.research_options["primary_keywords"]:
|
||||
st.warning("⚠️ Please enter primary keywords for research")
|
||||
return
|
||||
|
||||
try:
|
||||
# Create compact progress display
|
||||
progress_container = st.container()
|
||||
with progress_container:
|
||||
status_col, progress_col = st.columns([3, 1])
|
||||
with status_col:
|
||||
status_display = st.empty()
|
||||
status_display.info("🚀 Initializing research...")
|
||||
with progress_col:
|
||||
progress_bar = st.progress(0)
|
||||
|
||||
def update_progress(message, progress=None, level="info"):
|
||||
"""Update progress bar and status display.
|
||||
|
||||
Args:
|
||||
message (str): The message to display
|
||||
progress (float, optional): Progress value between 0 and 100. Will be converted to 0.0-1.0
|
||||
level (str, optional): Message level (info, warning, error, success)
|
||||
"""
|
||||
if progress is not None:
|
||||
# Convert percentage to decimal (0.0-1.0)
|
||||
progress = float(progress) / 100.0
|
||||
# Ensure progress stays within bounds
|
||||
progress = max(0.0, min(1.0, progress))
|
||||
progress_bar.progress(progress)
|
||||
|
||||
if level == "error":
|
||||
status_display.error(f"🚫 {message}")
|
||||
elif level == "warning":
|
||||
status_display.warning(f"⚠️ {message}")
|
||||
elif level == "success":
|
||||
status_display.success(f"✨ {message}")
|
||||
else:
|
||||
status_display.info(f"🔄 {message}")
|
||||
logger.debug(f"Progress update [{level}]: {message}")
|
||||
|
||||
# Execute search with all parameters
|
||||
try:
|
||||
update_progress("Starting search...", 0.25)
|
||||
logger.info(f"Executing web research with mode: {st.session_state.research_options['search_mode']}")
|
||||
|
||||
# Create base parameters
|
||||
research_params = {
|
||||
"search_keywords": st.session_state.research_options["primary_keywords"],
|
||||
"search_mode": st.session_state.research_options["search_mode"],
|
||||
"related_keywords": st.session_state.research_options["related_keywords"],
|
||||
"target_audience": st.session_state.research_options["target_audience"],
|
||||
"content_type": st.session_state.research_options["content_type"],
|
||||
"search_depth": st.session_state.research_options["search_depth"],
|
||||
"geo_location": st.session_state.research_options["geo_location"],
|
||||
"search_language": st.session_state.research_options["search_language"],
|
||||
"num_results": st.session_state.research_options["num_results"],
|
||||
"time_range": st.session_state.research_options["time_range"],
|
||||
"include_domains": st.session_state.research_options["include_domains"],
|
||||
"similar_url": st.session_state.research_options["similar_url"]
|
||||
}
|
||||
|
||||
# Add UI-specific parameters
|
||||
research_params.update({
|
||||
"status_container": status_display,
|
||||
"update_progress": update_progress
|
||||
})
|
||||
|
||||
# For AI search mode, ensure search_keywords is passed correctly
|
||||
if st.session_state.research_options["search_mode"] == "ai":
|
||||
research_params["tavily_params"] = {
|
||||
"max_results": st.session_state.research_options["num_results"],
|
||||
"search_depth": "advanced" if st.session_state.research_options["search_depth"] > 2 else "basic",
|
||||
"time_range": st.session_state.research_options["time_range"],
|
||||
"include_domains": st.session_state.research_options["include_domains"].split(",") if st.session_state.research_options["include_domains"] else [""]
|
||||
}
|
||||
# Pass search_keywords as a positional argument
|
||||
research_params["tavily_search_keywords"] = st.session_state.research_options["primary_keywords"]
|
||||
|
||||
# Execute the research
|
||||
web_research_result = gpt_web_researcher(**research_params)
|
||||
|
||||
if web_research_result:
|
||||
status_display.success("✨ Research completed!")
|
||||
|
||||
# Display results in an organized way
|
||||
with st.expander("📊 Research Results", expanded=False):
|
||||
st.write(web_research_result)
|
||||
else:
|
||||
st.warning("No results found for your search")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Research failed: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
st.error(f"🚫 Research failed: {error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in web research: {e}", exc_info=True)
|
||||
st.error("🚫 An unexpected error occurred. Please try again.")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in web research: {e}", exc_info=True)
|
||||
st.error("🚫 An unexpected error occurred. Please try again.")
|
||||
331
ToBeMigrated/alwrity_ui/navigation_styles.py
Normal file
331
ToBeMigrated/alwrity_ui/navigation_styles.py
Normal file
@@ -0,0 +1,331 @@
|
||||
import streamlit as st
|
||||
|
||||
def apply_navigation_styles():
|
||||
"""Apply navigation and UI setup specific styling."""
|
||||
st.markdown("""
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
/* Main app styling for navigation */
|
||||
.stApp {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
/* Compact layout styling with zero top padding when sub-tab selected */
|
||||
.main .block-container {
|
||||
padding-top: 0 !important; /* Remove all top padding */
|
||||
padding-bottom: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Remove extra padding and margins */
|
||||
.stMarkdown {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Header styling with zero margins when in sub-tab */
|
||||
.sub-tab-active h1, .sub-tab-active h2, .sub-tab-active h3 {
|
||||
display: none; /* Hide headers in sub-tab mode */
|
||||
}
|
||||
|
||||
/* Remove extra padding in containers */
|
||||
.stMarkdown {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
h1, h2, h3 {
|
||||
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-weight: 600;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem; /* Reduced from 1rem */
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* Reduce spacing between elements */
|
||||
.element-container {
|
||||
margin-bottom: 0.5rem; /* Reduced from 1rem */
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.stButton > button {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 0.25rem; /* Reduced from 0.5rem */
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.stButton > button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Input field styling */
|
||||
.stTextInput > div > div > input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
padding: 0.5rem 1rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Checkbox styling */
|
||||
.stCheckbox > label {
|
||||
font-weight: 500;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Expander styling */
|
||||
.streamlit-expanderHeader {
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Success message styling */
|
||||
.stSuccess {
|
||||
background: linear-gradient(135deg, #43c6ac 0%, #191654 100%);
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Error message styling */
|
||||
.stError {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e8e 100%);
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Info message styling */
|
||||
.stInfo {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Sidebar navigation styling */
|
||||
.sidebar-nav {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin: 0.2rem 0;
|
||||
border-radius: 4px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-button.active {
|
||||
background: #1565C0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Enhanced Sub-menu styling with minimal spacing */
|
||||
.sub-menu {
|
||||
padding-left: 1rem;
|
||||
margin: 0;
|
||||
border-left: 2px solid rgba(21, 101, 192, 0.3);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Sub-menu button styling with minimal gaps */
|
||||
.sub-menu .stButton > button {
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* Platform-specific button styles */
|
||||
.facebook-button .stButton > button {
|
||||
color: #4267B2;
|
||||
background: rgba(66, 103, 178, 0.1);
|
||||
}
|
||||
|
||||
.linkedin-button .stButton > button {
|
||||
color: #0077B5;
|
||||
background: rgba(0, 119, 181, 0.1);
|
||||
}
|
||||
|
||||
.twitter-button .stButton > button {
|
||||
color: #1DA1F2;
|
||||
background: rgba(29, 161, 242, 0.1);
|
||||
}
|
||||
|
||||
.instagram-button .stButton > button {
|
||||
color: #E1306C;
|
||||
background: rgba(225, 48, 108, 0.1);
|
||||
}
|
||||
|
||||
.youtube-button .stButton > button {
|
||||
color: #FF0000;
|
||||
background: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Platform-specific hover states */
|
||||
.facebook-button .stButton > button:hover {
|
||||
background: rgba(66, 103, 178, 0.2) !important;
|
||||
color: #4267B2 !important;
|
||||
}
|
||||
|
||||
.linkedin-button .stButton > button:hover {
|
||||
background: rgba(0, 119, 181, 0.2) !important;
|
||||
color: #0077B5 !important;
|
||||
}
|
||||
|
||||
.twitter-button .stButton > button:hover {
|
||||
background: rgba(29, 161, 242, 0.2) !important;
|
||||
color: #1DA1F2 !important;
|
||||
}
|
||||
|
||||
.instagram-button .stButton > button:hover {
|
||||
background: rgba(225, 48, 108, 0.2) !important;
|
||||
color: #E1306C !important;
|
||||
}
|
||||
|
||||
.youtube-button .stButton > button:hover {
|
||||
background: rgba(255, 0, 0, 0.2) !important;
|
||||
color: #FF0000 !important;
|
||||
}
|
||||
|
||||
/* Platform-specific active states */
|
||||
.facebook-button.active .stButton > button {
|
||||
background: #4267B2 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.linkedin-button.active .stButton > button {
|
||||
background: #0077B5 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.twitter-button.active .stButton > button {
|
||||
background: #1DA1F2 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.instagram-button.active .stButton > button {
|
||||
background: #E1306C !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.youtube-button.active .stButton > button {
|
||||
background: #FF0000 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Remove any extra spacing from button containers */
|
||||
.sub-menu .stButton {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sub-menu > div {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sub-menu .element-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure minimal gaps between elements */
|
||||
.sub-menu > div:not(:last-child) {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* Sidebar icon styling */
|
||||
.sidebar-icon {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sidebar-icon img {
|
||||
width: 80px !important;
|
||||
height: auto !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Additional compact layout overrides for specific pages */
|
||||
.compact-layout .main .block-container {
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Hide Streamlit elements for cleaner navigation */
|
||||
.stApp > header {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for navigation */
|
||||
.sidebar .stMarkdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar .stMarkdown::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar .stMarkdown::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar .stMarkdown::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
def apply_compact_layout():
|
||||
"""Apply compact layout styling for specific pages."""
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main .block-container {
|
||||
padding-top: 0.25rem !important;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
705
ToBeMigrated/alwrity_ui/seo_tools_dashboard.py
Normal file
705
ToBeMigrated/alwrity_ui/seo_tools_dashboard.py
Normal file
@@ -0,0 +1,705 @@
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
from typing import List, Dict, Any, Callable
|
||||
|
||||
# Import existing tools
|
||||
from lib.ai_seo_tools.seo_structured_data import ai_structured_data
|
||||
from lib.ai_seo_tools.content_title_generator import ai_title_generator
|
||||
from lib.ai_seo_tools.meta_desc_generator import metadesc_generator_main
|
||||
from lib.ai_seo_tools.image_alt_text_generator import alt_text_gen
|
||||
from lib.ai_seo_tools.opengraph_generator import og_tag_generator
|
||||
from lib.ai_seo_tools.optimize_images_for_upload import main_img_optimizer
|
||||
from lib.ai_seo_tools.google_pagespeed_insights import google_pagespeed_insights
|
||||
from lib.ai_seo_tools.on_page_seo_analyzer import analyze_onpage_seo
|
||||
from lib.ai_seo_tools.weburl_seo_checker import url_seo_checker
|
||||
from lib.ai_marketing_tools.ai_backlinker.backlinking_ui_streamlit import backlinking_ui
|
||||
from lib.ai_seo_tools.content_gap_analysis.ui import ContentGapAnalysisUI
|
||||
from lib.ai_seo_tools.content_gap_analysis.enhanced_ui import render_enhanced_content_gap_analysis
|
||||
from lib.ai_seo_tools.content_calendar.ui.dashboard import ContentCalendarDashboard
|
||||
from lib.ai_seo_tools.technical_seo_crawler import render_technical_seo_crawler
|
||||
|
||||
# Import additional tools
|
||||
from lib.ai_seo_tools.twitter_tags_generator import display_app as twitter_tags_app
|
||||
from lib.ai_seo_tools.sitemap_analysis import main as sitemap_analyzer
|
||||
from lib.ai_seo_tools.textstaty import analyze_text as readability_analyzer
|
||||
from lib.ai_seo_tools.wordcloud import generate_wordcloud
|
||||
|
||||
# Import new enterprise tools
|
||||
from ..ai_seo_tools.google_search_console_integration import render_gsc_integration
|
||||
from ..ai_seo_tools.ai_content_strategy import render_ai_content_strategy
|
||||
from ..ai_seo_tools.enterprise_seo_suite import render_enterprise_seo_suite
|
||||
|
||||
from lib.alwrity_ui.dashboard_styles import apply_dashboard_style, render_dashboard_header, render_category_header, render_card
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL CONFIGURATION FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def get_enterprise_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for enterprise tools."""
|
||||
return [
|
||||
{
|
||||
'name': '🎯 Enterprise SEO Suite',
|
||||
'description': 'Unified command center for comprehensive SEO management with AI-powered workflows',
|
||||
'function': render_enterprise_seo_suite,
|
||||
'features': ['Complete SEO audit workflows', 'AI-powered recommendations', 'Strategic planning', 'Performance tracking']
|
||||
},
|
||||
{
|
||||
'name': '📊 Google Search Console Intelligence',
|
||||
'description': 'AI-powered insights from Google Search Console data with content recommendations',
|
||||
'function': render_gsc_integration,
|
||||
'features': ['GSC data analysis', 'Content opportunities', 'Performance insights', 'Strategic recommendations']
|
||||
},
|
||||
{
|
||||
'name': '🧠 AI Content Strategy Generator',
|
||||
'description': 'Generate comprehensive content strategies using AI market intelligence',
|
||||
'function': render_ai_content_strategy,
|
||||
'features': ['Content pillar development', 'Topic cluster strategy', 'Content calendar planning', 'Distribution strategy']
|
||||
}
|
||||
]
|
||||
|
||||
def get_analytics_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for analytics tools."""
|
||||
return [
|
||||
{
|
||||
'name': '📊 Google Search Console Intelligence',
|
||||
'description': 'Deep analysis of GSC data with AI-powered content recommendations',
|
||||
'function': render_gsc_integration,
|
||||
'category': 'Search Analytics'
|
||||
},
|
||||
{
|
||||
'name': '🔍 Enhanced Content Gap Analysis',
|
||||
'description': 'Advanced competitor content analysis with AI insights',
|
||||
'function': lambda: render_enhanced_content_gap_analysis(),
|
||||
'category': 'Competitive Intelligence'
|
||||
},
|
||||
{
|
||||
'name': '📈 SEO Performance Tracker',
|
||||
'description': 'Track and analyze SEO performance with trend analysis',
|
||||
'function': lambda: st.info("SEO Performance Tracker - Coming soon with advanced metrics"),
|
||||
'category': 'Performance Analytics'
|
||||
}
|
||||
]
|
||||
|
||||
def get_technical_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for technical SEO tools."""
|
||||
return [
|
||||
{
|
||||
'name': '🔍 Technical SEO Crawler',
|
||||
'description': 'Comprehensive site-wide technical SEO analysis',
|
||||
'function': lambda: render_technical_seo_crawler(),
|
||||
'priority': 'High'
|
||||
},
|
||||
{
|
||||
'name': '📱 Mobile SEO Analyzer',
|
||||
'description': 'Mobile-specific SEO analysis and optimization',
|
||||
'function': lambda: st.info("Mobile SEO Analyzer - Advanced mobile optimization coming soon"),
|
||||
'priority': 'Medium'
|
||||
},
|
||||
{
|
||||
'name': '⚡ Core Web Vitals Optimizer',
|
||||
'description': 'Analyze and optimize Core Web Vitals performance',
|
||||
'function': lambda: st.info("Core Web Vitals Optimizer - Performance optimization coming soon"),
|
||||
'priority': 'High'
|
||||
},
|
||||
{
|
||||
'name': '🗺️ XML Sitemap Generator',
|
||||
'description': 'Generate and optimize XML sitemaps',
|
||||
'function': lambda: st.info("XML Sitemap Generator - Coming soon"),
|
||||
'priority': 'Medium'
|
||||
}
|
||||
]
|
||||
|
||||
def get_content_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for content and strategy tools."""
|
||||
return [
|
||||
{
|
||||
'name': '🧠 AI Content Strategy Generator',
|
||||
'description': 'Comprehensive content strategy with AI market intelligence',
|
||||
'function': render_ai_content_strategy,
|
||||
'type': 'Enterprise'
|
||||
},
|
||||
{
|
||||
'name': '📅 Content Calendar Planner',
|
||||
'description': 'AI-powered content calendar with SEO optimization',
|
||||
'function': lambda: render_content_calendar(),
|
||||
'type': 'Professional'
|
||||
},
|
||||
{
|
||||
'name': '🎯 Topic Cluster Generator',
|
||||
'description': 'Generate SEO topic clusters for content dominance',
|
||||
'function': lambda: st.info("Topic Cluster Generator - Advanced clustering coming soon"),
|
||||
'type': 'Professional'
|
||||
},
|
||||
{
|
||||
'name': '📊 Content Performance Analyzer',
|
||||
'description': 'Analyze content performance and optimization opportunities',
|
||||
'function': lambda: st.info("Content Performance Analyzer - Coming soon"),
|
||||
'type': 'Standard'
|
||||
}
|
||||
]
|
||||
|
||||
def get_basic_tools_config() -> List[Dict[str, Any]]:
|
||||
"""Get configuration for basic SEO tools."""
|
||||
return [
|
||||
{
|
||||
'name': '📝 Meta Description Generator',
|
||||
'description': 'Generate SEO-optimized meta descriptions',
|
||||
'function': lambda: metadesc_generator_main(),
|
||||
'category': 'Metadata'
|
||||
},
|
||||
{
|
||||
'name': '🎯 Content Title Generator',
|
||||
'description': 'Create compelling, SEO-friendly titles',
|
||||
'function': lambda: ai_title_generator(),
|
||||
'category': 'Content'
|
||||
},
|
||||
{
|
||||
'name': '🔗 OpenGraph Generator',
|
||||
'description': 'Generate social media OpenGraph tags',
|
||||
'function': lambda: og_tag_generator(),
|
||||
'category': 'Social'
|
||||
},
|
||||
{
|
||||
'name': '🖼️ Image Alt Text Generator',
|
||||
'description': 'Generate SEO-friendly image alt text',
|
||||
'function': lambda: alt_text_gen(),
|
||||
'category': 'Images'
|
||||
},
|
||||
{
|
||||
'name': '📋 Schema Markup Generator',
|
||||
'description': 'Generate structured data markup',
|
||||
'function': lambda: ai_structured_data(),
|
||||
'category': 'Technical'
|
||||
},
|
||||
{
|
||||
'name': '🔍 On-Page SEO Analyzer',
|
||||
'description': 'Comprehensive on-page SEO analysis',
|
||||
'function': lambda: analyze_onpage_seo(),
|
||||
'category': 'Analysis'
|
||||
},
|
||||
{
|
||||
'name': '🌐 URL SEO Checker',
|
||||
'description': 'Quick SEO check for any URL',
|
||||
'function': lambda: url_seo_checker(),
|
||||
'category': 'Analysis'
|
||||
}
|
||||
]
|
||||
|
||||
def get_tool_functions_mapping() -> Dict[str, Callable]:
|
||||
"""Get mapping of tool names to their functions for URL routing."""
|
||||
return {
|
||||
# Core content tools
|
||||
"structured_data": ai_structured_data,
|
||||
"blog_title": ai_title_generator,
|
||||
"meta_description": metadesc_generator_main,
|
||||
"alt_text": alt_text_gen,
|
||||
"opengraph": og_tag_generator,
|
||||
"image_optimizer": main_img_optimizer,
|
||||
|
||||
# Technical analysis tools
|
||||
"technical_seo_crawler": render_technical_seo_crawler,
|
||||
"pagespeed": google_pagespeed_insights,
|
||||
"onpage_seo": analyze_onpage_seo,
|
||||
"url_checker": url_seo_checker,
|
||||
"sitemap_analysis": sitemap_analyzer,
|
||||
|
||||
# Social media tools
|
||||
"twitter_tags": render_twitter_tags,
|
||||
|
||||
# Content analysis tools
|
||||
"readability_analyzer": render_readability_analyzer,
|
||||
"wordcloud_generator": render_wordcloud_generator,
|
||||
|
||||
# Advanced tools
|
||||
"backlinking": backlinking_ui,
|
||||
"content_gap_analysis": render_content_gap_analysis,
|
||||
"enhanced_content_gap_analysis": render_enhanced_content_gap_analysis_ui,
|
||||
"content_calendar": render_content_calendar,
|
||||
|
||||
# Tool combinations for workflow efficiency
|
||||
"content_optimization": lambda: run_tool_combination([
|
||||
ai_title_generator,
|
||||
metadesc_generator_main,
|
||||
ai_structured_data
|
||||
], "Content Optimization Suite"),
|
||||
"technical_audit": lambda: run_tool_combination([
|
||||
google_pagespeed_insights,
|
||||
analyze_onpage_seo,
|
||||
url_seo_checker
|
||||
], "Technical SEO Audit"),
|
||||
"image_optimization": lambda: run_tool_combination([
|
||||
alt_text_gen,
|
||||
main_img_optimizer
|
||||
], "Image Optimization Suite"),
|
||||
"social_optimization": lambda: run_tool_combination([
|
||||
og_tag_generator,
|
||||
render_twitter_tags
|
||||
], "Social Media Optimization")
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# INDIVIDUAL TOOL RENDERING FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def render_content_gap_analysis():
|
||||
"""Render the content gap analysis workflow interface."""
|
||||
ui = ContentGapAnalysisUI()
|
||||
ui.run()
|
||||
|
||||
def render_enhanced_content_gap_analysis_ui():
|
||||
"""Render the enhanced content gap analysis with advertools integration."""
|
||||
render_enhanced_content_gap_analysis()
|
||||
|
||||
def render_content_calendar():
|
||||
"""Render the content calendar dashboard with proper error handling."""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('content_calendar.log', mode='a')
|
||||
]
|
||||
)
|
||||
calendar_logger = logging.getLogger('content_calendar')
|
||||
|
||||
try:
|
||||
calendar_logger.info("Initializing Content Calendar Dashboard")
|
||||
dashboard = ContentCalendarDashboard()
|
||||
calendar_logger.info("Rendering Content Calendar Dashboard")
|
||||
dashboard.render()
|
||||
calendar_logger.info("Content Calendar Dashboard rendered successfully")
|
||||
except Exception as e:
|
||||
calendar_logger.error(f"Error rendering content calendar: {str(e)}", exc_info=True)
|
||||
st.error(f"An error occurred while loading the content calendar: {str(e)}")
|
||||
|
||||
def render_twitter_tags():
|
||||
"""Render the Twitter tags generator."""
|
||||
twitter_tags_app()
|
||||
|
||||
def render_readability_analyzer():
|
||||
"""Render the text readability analyzer."""
|
||||
st.title("📖 Text Readability Analyzer")
|
||||
st.write("Making Your Content Easy to Read")
|
||||
|
||||
text_input = st.text_area("Paste your text here:", height=200)
|
||||
|
||||
if st.button("Analyze Readability"):
|
||||
if text_input.strip():
|
||||
_display_readability_metrics(text_input)
|
||||
_display_readability_recommendations()
|
||||
else:
|
||||
st.error("Please enter text to analyze.")
|
||||
|
||||
def _display_readability_metrics(text: str):
|
||||
"""Display readability metrics for the given text."""
|
||||
from textstat import textstat
|
||||
|
||||
metrics = {
|
||||
"Flesch Reading Ease": textstat.flesch_reading_ease(text),
|
||||
"Flesch-Kincaid Grade Level": textstat.flesch_kincaid_grade(text),
|
||||
"Gunning Fog Index": textstat.gunning_fog(text),
|
||||
"SMOG Index": textstat.smog_index(text),
|
||||
"Automated Readability Index": textstat.automated_readability_index(text),
|
||||
"Coleman-Liau Index": textstat.coleman_liau_index(text),
|
||||
"Linsear Write Formula": textstat.linsear_write_formula(text),
|
||||
"Dale-Chall Readability Score": textstat.dale_chall_readability_score(text),
|
||||
"Readability Consensus": textstat.readability_consensus(text)
|
||||
}
|
||||
|
||||
st.subheader("Text Analysis Results")
|
||||
for metric, value in metrics.items():
|
||||
st.metric(metric, f"{value:.2f}")
|
||||
|
||||
def _display_readability_recommendations():
|
||||
"""Display readability recommendations."""
|
||||
st.subheader("Key Takeaways:")
|
||||
st.markdown("""
|
||||
* **Don't Be Afraid to Simplify!** Often, simpler language makes content more impactful and easier to digest.
|
||||
* **Aim for a Reading Level Appropriate for Your Audience:** Consider the education level, background, and familiarity of your readers.
|
||||
* **Use Short Sentences:** This makes your content more scannable and easier to read.
|
||||
* **Write for Everyone:** Accessibility should always be a priority. When in doubt, aim for clear, concise language!
|
||||
""")
|
||||
|
||||
def render_wordcloud_generator():
|
||||
"""Render the word cloud generator."""
|
||||
st.title("☁️ Word Cloud Generator")
|
||||
st.write("Visualize the most important words in your content")
|
||||
|
||||
text_input = st.text_area("Enter your text:", height=200)
|
||||
|
||||
if st.button("Generate Word Cloud"):
|
||||
if text_input.strip():
|
||||
_generate_and_display_wordcloud(text_input)
|
||||
_display_text_statistics(text_input)
|
||||
else:
|
||||
st.error("Please enter text to generate a word cloud.")
|
||||
|
||||
def _generate_and_display_wordcloud(text: str):
|
||||
"""Generate and display word cloud for the given text."""
|
||||
from wordcloud import WordCloud
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Create and generate a word cloud image
|
||||
wordcloud = WordCloud(width=800, height=400, background_color='white').generate(text)
|
||||
|
||||
# Display the word cloud
|
||||
st.subheader("Word Cloud Visualization")
|
||||
fig, ax = plt.subplots(figsize=(10, 5))
|
||||
ax.imshow(wordcloud, interpolation='bilinear')
|
||||
ax.axis('off')
|
||||
st.pyplot(fig)
|
||||
|
||||
def _display_text_statistics(text: str):
|
||||
"""Display basic text statistics."""
|
||||
st.subheader("Text Statistics")
|
||||
words = text.split()
|
||||
unique_words = set(words)
|
||||
st.metric("Total Words", len(words))
|
||||
st.metric("Unique Words", len(unique_words))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TAB RENDERING FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def render_enterprise_tab():
|
||||
"""Render the Enterprise Suite tab."""
|
||||
st.header("🏢 Enterprise SEO Command Center")
|
||||
st.markdown("**Unified SEO management for enterprise-level optimization**")
|
||||
|
||||
enterprise_tools = get_enterprise_tools_config()
|
||||
|
||||
# Display enterprise tools
|
||||
for tool in enterprise_tools:
|
||||
_render_enterprise_tool_card(tool)
|
||||
|
||||
# Render selected enterprise tool
|
||||
_render_selected_enterprise_tool(enterprise_tools)
|
||||
|
||||
def _render_enterprise_tool_card(tool: Dict[str, Any]):
|
||||
"""Render an individual enterprise tool card."""
|
||||
with st.expander(f"{tool['name']} - {tool['description']}", expanded=False):
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown("**Key Features:**")
|
||||
for feature in tool['features']:
|
||||
st.write(f"• {feature}")
|
||||
|
||||
with col2:
|
||||
if st.button(f"Launch {tool['name'].split()[1]}", key=f"enterprise_{tool['name']}", use_container_width=True):
|
||||
st.session_state.selected_enterprise_tool = tool['name']
|
||||
tool['function']()
|
||||
|
||||
def _render_selected_enterprise_tool(enterprise_tools: List[Dict[str, Any]]):
|
||||
"""Render the selected enterprise tool if any."""
|
||||
if 'selected_enterprise_tool' in st.session_state:
|
||||
selected_tool = next((tool for tool in enterprise_tools if tool['name'] == st.session_state.selected_enterprise_tool), None)
|
||||
if selected_tool:
|
||||
st.markdown("---")
|
||||
selected_tool['function']()
|
||||
|
||||
def render_analytics_tab():
|
||||
"""Render the Analytics & Intelligence tab."""
|
||||
st.header("📊 Analytics & Intelligence")
|
||||
st.markdown("**Advanced analytics and competitive intelligence tools**")
|
||||
|
||||
analytics_tools = get_analytics_tools_config()
|
||||
|
||||
# Group tools by category
|
||||
categories = _group_tools_by_category(analytics_tools)
|
||||
|
||||
for category, tools in categories.items():
|
||||
st.subheader(f"📊 {category}")
|
||||
|
||||
for tool in tools:
|
||||
_render_analytics_tool_row(tool)
|
||||
|
||||
def _group_tools_by_category(tools: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Group tools by their category."""
|
||||
categories = {}
|
||||
for tool in tools:
|
||||
category = tool['category']
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(tool)
|
||||
return categories
|
||||
|
||||
def _render_analytics_tool_row(tool: Dict[str, Any]):
|
||||
"""Render an analytics tool row."""
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}**")
|
||||
st.write(tool['description'])
|
||||
|
||||
with col2:
|
||||
if st.button("Launch", key=f"analytics_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
|
||||
def render_technical_tab():
|
||||
"""Render the Technical SEO tab."""
|
||||
st.header("🔧 Technical SEO")
|
||||
st.markdown("**Advanced technical SEO analysis and optimization tools**")
|
||||
|
||||
technical_tools = get_technical_tools_config()
|
||||
|
||||
# Display technical tools with priority indicators
|
||||
for tool in technical_tools:
|
||||
_render_technical_tool_row(tool)
|
||||
|
||||
def _render_technical_tool_row(tool: Dict[str, Any]):
|
||||
"""Render a technical tool row with priority indicator."""
|
||||
priority_color = "🔴" if tool['priority'] == 'High' else "🟡"
|
||||
|
||||
col1, col2, col3 = st.columns([2, 1, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}** {priority_color}")
|
||||
st.write(tool['description'])
|
||||
|
||||
with col2:
|
||||
st.write(f"**Priority:** {tool['priority']}")
|
||||
|
||||
with col3:
|
||||
if st.button("Launch", key=f"technical_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
|
||||
def render_content_tab():
|
||||
"""Render the Content & Strategy tab."""
|
||||
st.header("📝 Content & Strategy")
|
||||
st.markdown("**AI-powered content creation and strategy tools**")
|
||||
|
||||
content_tools = get_content_tools_config()
|
||||
|
||||
# Group by tool type
|
||||
tool_types = _group_tools_by_type(content_tools)
|
||||
|
||||
for tool_type, tools in tool_types.items():
|
||||
_render_content_tool_section(tool_type, tools)
|
||||
|
||||
def _group_tools_by_type(tools: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Group tools by their type."""
|
||||
tool_types = {}
|
||||
for tool in tools:
|
||||
tool_type = tool['type']
|
||||
if tool_type not in tool_types:
|
||||
tool_types[tool_type] = []
|
||||
tool_types[tool_type].append(tool)
|
||||
return tool_types
|
||||
|
||||
def _render_content_tool_section(tool_type: str, tools: List[Dict[str, Any]]):
|
||||
"""Render a content tool section."""
|
||||
type_color = {"Enterprise": "🏢", "Professional": "💼", "Standard": "📋"}
|
||||
st.subheader(f"{type_color.get(tool_type, '📋')} {tool_type} Tools")
|
||||
|
||||
for tool in tools:
|
||||
col1, col2 = st.columns([3, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"**{tool['name']}**")
|
||||
st.write(tool['description'])
|
||||
|
||||
with col2:
|
||||
if st.button("Launch", key=f"content_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
|
||||
def render_basic_tools_tab():
|
||||
"""Render the Basic Tools tab."""
|
||||
st.header("🎯 Basic SEO Tools")
|
||||
st.markdown("**Essential SEO tools for quick optimization tasks**")
|
||||
|
||||
basic_tools = get_basic_tools_config()
|
||||
|
||||
# Group basic tools by category
|
||||
basic_categories = _group_tools_by_category(basic_tools)
|
||||
|
||||
# Display in columns for better layout
|
||||
_render_basic_tools_in_columns(basic_categories)
|
||||
|
||||
def _render_basic_tools_in_columns(basic_categories: Dict[str, List[Dict[str, Any]]]):
|
||||
"""Render basic tools in two columns."""
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
categories_list = list(basic_categories.items())
|
||||
mid_point = len(categories_list) // 2
|
||||
|
||||
with col1:
|
||||
for category, tools in categories_list[:mid_point]:
|
||||
_render_basic_tool_category(category, tools)
|
||||
|
||||
with col2:
|
||||
for category, tools in categories_list[mid_point:]:
|
||||
_render_basic_tool_category(category, tools)
|
||||
|
||||
def _render_basic_tool_category(category: str, tools: List[Dict[str, Any]]):
|
||||
"""Render a basic tool category."""
|
||||
st.subheader(f"📂 {category}")
|
||||
for tool in tools:
|
||||
if st.button(f"{tool['name']}", key=f"basic_{tool['name']}", use_container_width=True):
|
||||
tool['function']()
|
||||
st.caption(tool['description'])
|
||||
st.markdown("---")
|
||||
|
||||
def render_enterprise_features_footer():
|
||||
"""Render the enterprise features footer."""
|
||||
st.markdown("---")
|
||||
st.markdown("### 🚀 Enterprise SEO Features")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.info("""
|
||||
**🏢 Enterprise Suite**
|
||||
- Unified SEO workflows
|
||||
- AI-powered insights
|
||||
- Strategic planning
|
||||
- Performance tracking
|
||||
""")
|
||||
|
||||
with col2:
|
||||
st.info("""
|
||||
**📊 Advanced Analytics**
|
||||
- GSC integration
|
||||
- Competitive intelligence
|
||||
- Content gap analysis
|
||||
- Performance insights
|
||||
""")
|
||||
|
||||
with col3:
|
||||
st.info("""
|
||||
**🧠 AI Strategy**
|
||||
- Content strategy generation
|
||||
- Topic cluster planning
|
||||
- Distribution optimization
|
||||
- Market intelligence
|
||||
""")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN DASHBOARD FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def render_seo_tools_dashboard():
|
||||
"""Render comprehensive SEO tools dashboard with enterprise features."""
|
||||
st.title("🚀 Alwrity AI SEO Tools")
|
||||
st.markdown("**Enterprise-level SEO tools powered by artificial intelligence**")
|
||||
|
||||
# Create tabs for different tool categories
|
||||
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
||||
"🏢 Enterprise Suite",
|
||||
"📊 Analytics & Intelligence",
|
||||
"🔧 Technical SEO",
|
||||
"📝 Content & Strategy",
|
||||
"🎯 Basic Tools"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
render_enterprise_tab()
|
||||
|
||||
with tab2:
|
||||
render_analytics_tab()
|
||||
|
||||
with tab3:
|
||||
render_technical_tab()
|
||||
|
||||
with tab4:
|
||||
render_content_tab()
|
||||
|
||||
with tab5:
|
||||
render_basic_tools_tab()
|
||||
|
||||
# Add footer with enterprise features highlight
|
||||
render_enterprise_features_footer()
|
||||
|
||||
def ai_seo_tools():
|
||||
"""Main entry point for SEO tools dashboard with premium glassmorphic design."""
|
||||
logger.info("Starting SEO Tools Dashboard")
|
||||
|
||||
# Apply common dashboard styling
|
||||
apply_dashboard_style()
|
||||
|
||||
# Check if a specific tool is selected
|
||||
selected_tool = st.query_params.get("tool")
|
||||
|
||||
if selected_tool:
|
||||
_handle_selected_tool(selected_tool)
|
||||
else:
|
||||
# Show the dashboard if no tool is selected
|
||||
render_seo_tools_dashboard()
|
||||
|
||||
def _handle_selected_tool(selected_tool: str):
|
||||
"""Handle rendering of a specific selected tool."""
|
||||
tool_functions = get_tool_functions_mapping()
|
||||
|
||||
if selected_tool in tool_functions:
|
||||
# Clear any existing content
|
||||
st.empty()
|
||||
# Execute the selected tool's function
|
||||
tool_functions[selected_tool]()
|
||||
else:
|
||||
st.error(f"Tool '{selected_tool}' is not available or under development.")
|
||||
st.info("Please select a different tool from the dashboard.")
|
||||
render_seo_tools_dashboard()
|
||||
|
||||
def run_tool_combination(tools: List[Callable], combination_name: str):
|
||||
"""Run a combination of tools and provide cross-tool analysis."""
|
||||
st.markdown(f"# {combination_name}")
|
||||
st.markdown("Comprehensive SEO analysis workflow")
|
||||
|
||||
# Create tabs for each tool in the combination
|
||||
tab_names = _generate_tab_names(tools)
|
||||
tabs = st.tabs(tab_names)
|
||||
|
||||
# Run each tool in its own tab
|
||||
_execute_tools_in_tabs(tabs, tools)
|
||||
|
||||
# Add cross-tool analysis section
|
||||
_render_analysis_summary()
|
||||
|
||||
def _generate_tab_names(tools: List[Callable]) -> List[str]:
|
||||
"""Generate tab names for tool combination."""
|
||||
tab_names = []
|
||||
for i, tool in enumerate(tools):
|
||||
if hasattr(tool, '__name__'):
|
||||
tab_names.append(tool.__name__.replace('_', ' ').title())
|
||||
else:
|
||||
tab_names.append(f"Step {i+1}")
|
||||
return tab_names
|
||||
|
||||
def _execute_tools_in_tabs(tabs: List, tools: List[Callable]):
|
||||
"""Execute tools in their respective tabs."""
|
||||
for tab, tool in zip(tabs, tools):
|
||||
with tab:
|
||||
try:
|
||||
tool()
|
||||
except Exception as e:
|
||||
st.error(f"Error running tool: {str(e)}")
|
||||
logger.error(f"Error in tool combination: {str(e)}")
|
||||
|
||||
def _render_analysis_summary():
|
||||
"""Render the analysis summary section."""
|
||||
with st.expander("📊 Analysis Summary", expanded=True):
|
||||
st.markdown("""
|
||||
### Key Recommendations:
|
||||
1. **Content Optimization**: Ensure your titles and meta descriptions are keyword-optimized
|
||||
2. **Technical Performance**: Address any speed or technical issues identified
|
||||
3. **Structured Data**: Implement schema markup for better search visibility
|
||||
4. **Social Optimization**: Optimize social sharing tags for better engagement
|
||||
|
||||
### Next Steps:
|
||||
- Implement the recommendations from each tool
|
||||
- Monitor your rankings and traffic after changes
|
||||
- Regularly audit your content using these tools
|
||||
""")
|
||||
|
||||
# Add export functionality placeholder
|
||||
if st.button("📥 Export Analysis Report", use_container_width=True):
|
||||
st.info("Export functionality is being developed. Save your results manually for now.")
|
||||
973
ToBeMigrated/alwrity_ui/settings_page.py
Normal file
973
ToBeMigrated/alwrity_ui/settings_page.py
Normal file
@@ -0,0 +1,973 @@
|
||||
import streamlit as st
|
||||
from loguru import logger
|
||||
import asyncio
|
||||
from lib.web_crawlers.async_web_crawler import AsyncWebCrawlerService
|
||||
from lib.personalization.style_analyzer import StyleAnalyzer
|
||||
from lib.alwrity_ui.dashboard_styles import apply_dashboard_style, render_dashboard_header
|
||||
import sys
|
||||
|
||||
# Configure logger
|
||||
logger.remove() # Remove default handler
|
||||
logger.add(
|
||||
"logs/settings_page.log",
|
||||
rotation="500 MB",
|
||||
retention="10 days",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
|
||||
backtrace=True,
|
||||
diagnose=True
|
||||
)
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="INFO",
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{message}</cyan>"
|
||||
)
|
||||
|
||||
def display_style_analysis(analysis_results: dict):
|
||||
"""Display the style analysis results in a structured format with premium styling."""
|
||||
try:
|
||||
# Writing Style Section
|
||||
st.markdown("""
|
||||
<div class="analysis-section">
|
||||
<div class="section-icon">🎨</div>
|
||||
<h3>Writing Style Analysis</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
writing_style = analysis_results.get("writing_style", {})
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Tone:</span>
|
||||
<span class="metric-value">{writing_style.get("tone", "N/A")}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Voice:</span>
|
||||
<span class="metric-value">{writing_style.get("voice", "N/A")}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
with col2:
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Complexity:</span>
|
||||
<span class="metric-value">{writing_style.get("complexity", "N/A")}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Engagement:</span>
|
||||
<span class="metric-value">{writing_style.get("engagement_level", "N/A")}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Content Characteristics Section
|
||||
st.markdown("""
|
||||
<div class="analysis-section">
|
||||
<div class="section-icon">📊</div>
|
||||
<h3>Content Characteristics</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
content_chars = analysis_results.get("content_characteristics", {})
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Sentence Structure:</span>
|
||||
<span class="metric-value">{content_chars.get("sentence_structure", "N/A")}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Vocabulary Level:</span>
|
||||
<span class="metric-value">{content_chars.get("vocabulary_level", "N/A")}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
with col2:
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Organization:</span>
|
||||
<span class="metric-value">{content_chars.get("paragraph_organization", "N/A")}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Content Flow:</span>
|
||||
<span class="metric-value">{content_chars.get("content_flow", "N/A")}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Target Audience Section
|
||||
st.markdown("""
|
||||
<div class="analysis-section">
|
||||
<div class="section-icon">🎯</div>
|
||||
<h3>Target Audience</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
target_audience = analysis_results.get("target_audience", {})
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Demographics:</span>
|
||||
<span class="metric-value">{', '.join(target_audience.get("demographics", ["N/A"]))}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Expertise Level:</span>
|
||||
<span class="metric-value">{target_audience.get("expertise_level", "N/A")}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
with col2:
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Industry Focus:</span>
|
||||
<span class="metric-value">{target_audience.get("industry_focus", "N/A")}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Geographic Focus:</span>
|
||||
<span class="metric-value">{target_audience.get("geographic_focus", "N/A")}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Recommended Settings Section
|
||||
st.markdown("""
|
||||
<div class="analysis-section">
|
||||
<div class="section-icon">⚙️</div>
|
||||
<h3>Recommended Settings</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
recommended = analysis_results.get("recommended_settings", {})
|
||||
st.markdown(f"""
|
||||
<div class="recommendations-grid">
|
||||
<div class="recommendation-card">
|
||||
<div class="rec-icon">🎭</div>
|
||||
<div class="rec-label">Writing Tone</div>
|
||||
<div class="rec-value">{recommended.get("writing_tone", "N/A")}</div>
|
||||
</div>
|
||||
<div class="recommendation-card">
|
||||
<div class="rec-icon">👥</div>
|
||||
<div class="rec-label">Target Audience</div>
|
||||
<div class="rec-value">{recommended.get("target_audience", "N/A")}</div>
|
||||
</div>
|
||||
<div class="recommendation-card">
|
||||
<div class="rec-icon">📝</div>
|
||||
<div class="rec-label">Content Type</div>
|
||||
<div class="rec-value">{recommended.get("content_type", "N/A")}</div>
|
||||
</div>
|
||||
<div class="recommendation-card">
|
||||
<div class="rec-icon">🎨</div>
|
||||
<div class="rec-label">Creativity Level</div>
|
||||
<div class="rec-value">{recommended.get("creativity_level", "N/A")}</div>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error displaying style analysis: {str(e)}")
|
||||
st.error(f"Error displaying analysis results: {str(e)}")
|
||||
|
||||
def render_settings_page():
|
||||
"""Renders the settings page with premium glassmorphic design and all configuration options in tabs"""
|
||||
|
||||
# Apply common dashboard styling
|
||||
apply_dashboard_style()
|
||||
|
||||
# Add settings-specific CSS for tabs and form elements
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* Settings-specific overrides and additions */
|
||||
.main .block-container {
|
||||
padding: 1rem 1.5rem 1.5rem 1.5rem !important;
|
||||
margin: 1rem auto !important;
|
||||
}
|
||||
|
||||
.element-container {
|
||||
margin-bottom: 0.3rem !important;
|
||||
}
|
||||
|
||||
.stMarkdown {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
margin-bottom: 0.3rem !important;
|
||||
}
|
||||
|
||||
/* Enhanced tab styling for settings */
|
||||
.stTabs [data-baseweb="tab-list"] {
|
||||
gap: 0.5rem !important;
|
||||
background: rgba(255, 255, 255, 0.35) !important;
|
||||
backdrop-filter: blur(30px) !important;
|
||||
border-radius: 18px !important;
|
||||
padding: 0.8rem !important;
|
||||
border: 3px solid rgba(255, 255, 255, 0.4) !important;
|
||||
margin-bottom: 1.5rem !important;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.25),
|
||||
inset 0 3px 0 rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
.stTabs [data-baseweb="tab"] {
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
backdrop-filter: blur(25px) !important;
|
||||
border-radius: 14px !important;
|
||||
color: #ffffff !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.35) !important;
|
||||
padding: 1rem 2rem !important;
|
||||
font-weight: 800 !important;
|
||||
font-size: 1.05rem !important;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
margin: 0 !important;
|
||||
min-height: 50px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
text-shadow: 0 3px 6px rgba(0, 0, 0, 0.6) !important;
|
||||
box-shadow:
|
||||
0 8px 25px rgba(0, 0, 0, 0.2),
|
||||
inset 0 2px 0 rgba(255, 255, 255, 0.4) !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
.stTabs [data-baseweb="tab"]:hover {
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
backdrop-filter: blur(30px) !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255, 255, 255, 0.5) !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow:
|
||||
0 15px 35px rgba(0, 0, 0, 0.3),
|
||||
inset 0 3px 0 rgba(255, 255, 255, 0.5) !important;
|
||||
}
|
||||
|
||||
.stTabs [aria-selected="true"] {
|
||||
background: rgba(255, 255, 255, 0.5) !important;
|
||||
backdrop-filter: blur(35px) !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(255, 255, 255, 0.6) !important;
|
||||
box-shadow:
|
||||
0 15px 35px rgba(0, 0, 0, 0.3),
|
||||
inset 0 3px 0 rgba(255, 255, 255, 0.6),
|
||||
0 0 0 2px rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
font-weight: 900 !important;
|
||||
text-shadow: 0 3px 8px rgba(0, 0, 0, 0.7) !important;
|
||||
}
|
||||
|
||||
/* Settings sections */
|
||||
.settings-section {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(25px);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
color: #ffffff !important;
|
||||
font-size: 1.6em !important;
|
||||
font-weight: 800 !important;
|
||||
margin-bottom: 1.2rem !important;
|
||||
margin-top: 0 !important;
|
||||
text-shadow: 0 3px 12px rgba(0, 0, 0, 0.5) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
letter-spacing: -0.01em;
|
||||
padding-bottom: 0.8rem;
|
||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
/* Form elements with maximum visibility */
|
||||
.stSelectbox > div > div,
|
||||
.stSelectbox div[data-baseweb="select"] > div {
|
||||
background: rgba(255, 255, 255, 0.35) !important;
|
||||
backdrop-filter: blur(30px) !important;
|
||||
border: 3px solid rgba(255, 255, 255, 0.5) !important;
|
||||
border-radius: 12px !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.2),
|
||||
inset 0 2px 0 rgba(255, 255, 255, 0.4) !important;
|
||||
min-height: 45px !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 1rem !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
.stTextInput > div > div > input,
|
||||
.stTextArea > div > div > textarea,
|
||||
.stNumberInput > div > div > input {
|
||||
background: rgba(255, 255, 255, 0.35) !important;
|
||||
backdrop-filter: blur(30px) !important;
|
||||
border: 3px solid rgba(255, 255, 255, 0.5) !important;
|
||||
border-radius: 12px !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.2),
|
||||
inset 0 2px 0 rgba(255, 255, 255, 0.4) !important;
|
||||
min-height: 45px !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 1rem !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
|
||||
.stTextInput > div > div > input:focus,
|
||||
.stTextArea > div > div > textarea:focus,
|
||||
.stNumberInput > div > div > input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.7) !important;
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(255, 255, 255, 0.4),
|
||||
0 15px 35px rgba(0, 0, 0, 0.25) !important;
|
||||
background: rgba(255, 255, 255, 0.4) !important;
|
||||
}
|
||||
|
||||
.stTextInput > div > div > input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.8) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Enhanced labels */
|
||||
.stMarkdown p, .stSelectbox label, .stTextInput label, .stTextArea label, .stSlider label, .stNumberInput label {
|
||||
color: #ffffff !important;
|
||||
font-weight: 800 !important;
|
||||
text-shadow:
|
||||
0 2px 6px rgba(0, 0, 0, 0.5),
|
||||
0 1px 3px rgba(0, 0, 0, 0.7) !important;
|
||||
font-size: 1.05rem !important;
|
||||
margin-bottom: 0.4rem !important;
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Slider styling */
|
||||
.stSlider > div > div > div {
|
||||
background: rgba(255, 255, 255, 0.5) !important;
|
||||
border-radius: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
|
||||
.stSlider > div > div > div > div {
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25) !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
/* Analysis results styling */
|
||||
.analysis-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 2rem 0 1rem 0;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 1.5em;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.analysis-section h3 {
|
||||
color: #ffffff;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.analysis-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.8rem;
|
||||
padding-bottom: 0.8rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.metric-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 500;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.recommendations-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.recommendation-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.recommendation-card:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.rec-icon {
|
||||
font-size: 2em;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.rec-label {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.rec-value {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-size: 1em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Status messages */
|
||||
.stSuccess {
|
||||
background: rgba(46, 160, 67, 0.2) !important;
|
||||
backdrop-filter: blur(15px) !important;
|
||||
border: 1px solid rgba(46, 160, 67, 0.3) !important;
|
||||
border-radius: 12px !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.stError {
|
||||
background: rgba(255, 75, 75, 0.2) !important;
|
||||
backdrop-filter: blur(15px) !important;
|
||||
border: 1px solid rgba(255, 75, 75, 0.3) !important;
|
||||
border-radius: 12px !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.stWarning {
|
||||
background: rgba(255, 196, 9, 0.2) !important;
|
||||
backdrop-filter: blur(15px) !important;
|
||||
border: 1px solid rgba(255, 196, 9, 0.3) !important;
|
||||
border-radius: 12px !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.stStatus {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
backdrop-filter: blur(15px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Use the common dashboard header
|
||||
render_dashboard_header(
|
||||
"⚙️ Settings & Configuration",
|
||||
"Customize your AI experience with precision controls for content generation, personalization, and optimization. Fine-tune every aspect to match your unique requirements and style."
|
||||
)
|
||||
|
||||
# Create tabs for different settings categories with premium styling
|
||||
tabs = st.tabs([
|
||||
"📝 Content",
|
||||
"🖼️ Images",
|
||||
"🤖 LLM",
|
||||
"🔍 Search",
|
||||
"🎨 AI Personalization"
|
||||
])
|
||||
|
||||
# Content Settings Tab
|
||||
with tabs[0]:
|
||||
st.markdown("""
|
||||
<div class="settings-section">
|
||||
<h2>📝 Content Personalization</h2>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
blog_length = st.text_input(
|
||||
"**Content Length (words)**",
|
||||
value="2000",
|
||||
key="settings_blog_length",
|
||||
help="Approximate word count for blogs. Note: Actual length may vary based on GPT provider and max token count."
|
||||
)
|
||||
|
||||
blog_tone_options = ["Casual", "Professional", "How-to", "Beginner", "Research", "Programming", "Social Media", "Customize"]
|
||||
blog_tone = st.selectbox(
|
||||
"**Content Tone**",
|
||||
options=blog_tone_options,
|
||||
key="settings_blog_tone",
|
||||
help="Select the desired tone for the blog content."
|
||||
)
|
||||
|
||||
# Initialize custom_tone variable
|
||||
custom_tone = ""
|
||||
if blog_tone == "Customize":
|
||||
custom_tone = st.text_input(
|
||||
"Enter the tone of your content",
|
||||
key="settings_custom_tone",
|
||||
help="Specify the tone of your content."
|
||||
)
|
||||
if custom_tone:
|
||||
blog_tone = custom_tone
|
||||
else:
|
||||
st.warning("Please specify the tone of your content.")
|
||||
|
||||
blog_demographic_options = ["Professional", "Gen-Z", "Tech-savvy", "Student", "Digital Marketing", "Customize"]
|
||||
blog_demographic = st.selectbox(
|
||||
"**Target Audience**",
|
||||
options=blog_demographic_options,
|
||||
key="settings_blog_demographic",
|
||||
help="Select the primary audience for the blog content."
|
||||
)
|
||||
|
||||
with col2:
|
||||
blog_type = st.selectbox(
|
||||
"**Content Type**",
|
||||
options=["Informational", "Commercial", "Company", "News", "Finance", "Competitor", "Programming", "Scholar"],
|
||||
key="settings_blog_type",
|
||||
help="Select the category that best describes the blog content."
|
||||
)
|
||||
|
||||
blog_language = st.selectbox(
|
||||
"**Content Language**",
|
||||
options=["English", "Spanish", "German", "Chinese", "Arabic", "Nepali", "Hindi", "Hindustani", "Customize"],
|
||||
key="settings_blog_language",
|
||||
help="Select the language in which the blog will be written."
|
||||
)
|
||||
|
||||
blog_output_format = st.selectbox(
|
||||
"**Content Output Format**",
|
||||
options=["markdown", "HTML", "plaintext"],
|
||||
key="settings_blog_output_format",
|
||||
help="Select the format for the blog output."
|
||||
)
|
||||
|
||||
# Images Settings Tab
|
||||
with tabs[1]:
|
||||
st.markdown("""
|
||||
<div class="settings-section">
|
||||
<h2>🖼️ Images Personalization</h2>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
image_generation_model = st.selectbox(
|
||||
"**Image Generation Model**",
|
||||
options=["stable-diffusion", "dalle2", "dalle3"],
|
||||
key="settings_image_model",
|
||||
help="Select the model to generate images for the blog."
|
||||
)
|
||||
|
||||
with col2:
|
||||
number_of_blog_images = st.number_input(
|
||||
"**Number of Blog Images**",
|
||||
value=1,
|
||||
min_value=1,
|
||||
max_value=10,
|
||||
key="settings_number_of_images",
|
||||
help="Specify the number of images to include in the blog."
|
||||
)
|
||||
|
||||
# LLM Settings Tab
|
||||
with tabs[2]:
|
||||
st.markdown("""
|
||||
<div class="settings-section">
|
||||
<h2>🤖 LLM Personalization</h2>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
gpt_provider = st.selectbox(
|
||||
"**GPT Provider**",
|
||||
options=["google", "openai", "minstral"],
|
||||
key="settings_gpt_provider",
|
||||
help="Select the provider for the GPT model."
|
||||
)
|
||||
|
||||
model = st.text_input(
|
||||
"**Model**",
|
||||
value="gemini-1.5-flash-latest",
|
||||
key="settings_model",
|
||||
help="Specify the model version to use from the selected provider."
|
||||
)
|
||||
|
||||
temperature = st.slider(
|
||||
"**Temperature**",
|
||||
min_value=0.1,
|
||||
max_value=1.0,
|
||||
value=0.7,
|
||||
step=0.1,
|
||||
key="settings_temperature",
|
||||
help="Controls the creativity level of the generated text."
|
||||
)
|
||||
|
||||
max_tokens = st.selectbox(
|
||||
"**Max Tokens**",
|
||||
options=[500, 1000, 2000, 4000, 16000, 32000, 64000],
|
||||
index=3,
|
||||
key="settings_max_tokens",
|
||||
help="Maximum length of the output sequence."
|
||||
)
|
||||
|
||||
with col2:
|
||||
top_p = st.slider(
|
||||
"**Top-p**",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=0.9,
|
||||
step=0.1,
|
||||
key="settings_top_p",
|
||||
help="Controls diversity in text generation."
|
||||
)
|
||||
|
||||
frequency_penalty = st.slider(
|
||||
"**Frequency Penalty**",
|
||||
min_value=0.0,
|
||||
max_value=2.0,
|
||||
value=1.0,
|
||||
step=0.1,
|
||||
key="settings_frequency_penalty",
|
||||
help="Reduces word repetition in output."
|
||||
)
|
||||
|
||||
# Search Settings Tab
|
||||
with tabs[3]:
|
||||
st.markdown("""
|
||||
<div class="settings-section">
|
||||
<h2>🔍 Search Engine Personalization</h2>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
geographic_location = st.selectbox(
|
||||
"**Geographic Location**",
|
||||
options=["us", "in", "fr", "cn"],
|
||||
key="settings_geographic_location",
|
||||
help="Select the geographic location for tailoring search results."
|
||||
)
|
||||
|
||||
search_language = st.selectbox(
|
||||
"**Search Language**",
|
||||
options=["en", "zn-cn", "de", "hi"],
|
||||
key="settings_search_language",
|
||||
help="Select the language for the search results."
|
||||
)
|
||||
|
||||
number_of_results = st.number_input(
|
||||
"**Number of Results**",
|
||||
value=10,
|
||||
min_value=1,
|
||||
max_value=20,
|
||||
key="settings_number_of_results",
|
||||
help="Specify the number of search results to retrieve."
|
||||
)
|
||||
|
||||
with col2:
|
||||
time_range = st.selectbox(
|
||||
"**Time Range**",
|
||||
options=["anytime", "past day", "past week", "past month", "past year"],
|
||||
key="settings_time_range",
|
||||
help="Select the time range for filtering search results."
|
||||
)
|
||||
|
||||
include_domains = st.text_input(
|
||||
"**Include Domains**",
|
||||
value="",
|
||||
key="settings_include_domains",
|
||||
help="List specific domains to include in search results (comma-separated)."
|
||||
)
|
||||
|
||||
similar_url = st.text_input(
|
||||
"**Similar URL**",
|
||||
value="",
|
||||
key="settings_similar_url",
|
||||
help="Provide a URL to find similar results."
|
||||
)
|
||||
|
||||
# AI Personalization Tab
|
||||
with tabs[4]:
|
||||
st.markdown("""
|
||||
<div class="settings-section">
|
||||
<h2>🎨 AI Style Analysis</h2>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""
|
||||
<div class="analysis-card" style="margin-bottom: 2rem;">
|
||||
<p style="color: rgba(255, 255, 255, 0.9); font-size: 1.1em; margin: 0; line-height: 1.6;">
|
||||
Enter a website URL or provide content samples to analyze your writing style and get personalized recommendations for optimal AI content generation.
|
||||
</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Create two columns for the layout
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
# Website URL input
|
||||
st.markdown("#### 🌐 Website URL Analysis")
|
||||
url = st.text_input(
|
||||
"Enter your website URL",
|
||||
placeholder="https://example.com",
|
||||
key="settings_website_url",
|
||||
help="Provide your website URL to analyze your content style. Leave empty if you want to provide written samples instead."
|
||||
)
|
||||
|
||||
# Alternative: Written samples
|
||||
if not url:
|
||||
st.markdown("#### 📝 Written Samples")
|
||||
st.markdown("""
|
||||
<div class="analysis-card">
|
||||
<p style="color: rgba(255, 255, 255, 0.9); margin: 0; line-height: 1.6;">
|
||||
No website URL? No problem! You can provide written samples of your content instead.
|
||||
Share your best articles, blog posts, or any content that represents your writing style.
|
||||
</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
samples = st.text_area(
|
||||
"Paste your content samples here",
|
||||
key="settings_content_samples",
|
||||
help="Paste 2-3 samples of your best content. This helps ALwrity understand your writing style.",
|
||||
height=200
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("#### 🎯 Analysis Features")
|
||||
st.markdown("""
|
||||
<div class="analysis-card">
|
||||
<div style="color: rgba(255, 255, 255, 0.9); line-height: 1.6;">
|
||||
<p><strong>✨ Writing Style:</strong> Tone, voice, complexity analysis</p>
|
||||
<p><strong>📊 Content Analysis:</strong> Structure and vocabulary assessment</p>
|
||||
<p><strong>🎯 Audience Insights:</strong> Target demographic identification</p>
|
||||
<p><strong>⚙️ AI Recommendations:</strong> Personalized settings optimization</p>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Add spacing between categories
|
||||
st.markdown("<div style='height: 20px'></div>", unsafe_allow_html=True)
|
||||
|
||||
if st.button("🎨 Analyze Writing Style", use_container_width=True, key="settings_analyze_style", type="primary"):
|
||||
if url:
|
||||
with st.status("Starting style analysis...", expanded=True) as status:
|
||||
try:
|
||||
# Step 1: Initialize crawler
|
||||
status.update(label="Step 1/4: Initializing web crawler...", state="running")
|
||||
crawler_service = AsyncWebCrawlerService()
|
||||
|
||||
# Step 2: Crawl website
|
||||
status.update(label="Step 2/4: Crawling website content...", state="running")
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
result = loop.run_until_complete(crawler_service.crawl_website(url))
|
||||
loop.close()
|
||||
|
||||
if result.get('success', False):
|
||||
content = result.get('content', {})
|
||||
|
||||
# Step 3: Initialize style analyzer
|
||||
status.update(label="Step 3/4: Analyzing content style...", state="running")
|
||||
style_analyzer = StyleAnalyzer()
|
||||
|
||||
# Step 4: Perform style analysis
|
||||
status.update(label="Step 4/4: Generating style recommendations...", state="running")
|
||||
style_analysis = style_analyzer.analyze_content_style(content)
|
||||
|
||||
if style_analysis.get('error'):
|
||||
status.update(label="Analysis failed", state="error")
|
||||
st.error(f"Style analysis failed: {style_analysis['error']}")
|
||||
else:
|
||||
status.update(label="Analysis complete!", state="complete")
|
||||
# Display style analysis results
|
||||
display_style_analysis(style_analysis)
|
||||
|
||||
# Display original content in tabs
|
||||
tab1, tab2, tab3 = st.tabs(["📄 Content", "📋 Metadata", "🔗 Links"])
|
||||
|
||||
with tab1:
|
||||
st.markdown("#### Main Content")
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div style="color: rgba(255, 255, 255, 0.9); line-height: 1.6;">
|
||||
{content.get('main_content', 'No content found')}
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with tab2:
|
||||
st.markdown("#### Website Metadata")
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Title:</span>
|
||||
<span class="metric-value">{content.get('title', 'No title found')}</span>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">Description:</span>
|
||||
<span class="metric-value">{content.get('description', 'No description found')}</span>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with tab3:
|
||||
st.markdown("#### Extracted Links")
|
||||
links = content.get('links', [])
|
||||
if links:
|
||||
for link in links[:10]: # Show first 10 links
|
||||
st.markdown(f"""
|
||||
<div class="analysis-card" style="margin-bottom: 0.5rem;">
|
||||
<a href="{link.get('href', '')}" target="_blank" style="color: rgba(255, 255, 255, 0.9); text-decoration: none;">
|
||||
{link.get('text', 'No text')[:80]}...
|
||||
</a>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
st.markdown("No links found in the content.")
|
||||
else:
|
||||
status.update(label="Crawling failed", state="error")
|
||||
st.error("Failed to crawl the website. Please check the URL and try again.")
|
||||
except Exception as e:
|
||||
status.update(label="Analysis failed", state="error")
|
||||
st.error(f"An error occurred during analysis: {str(e)}")
|
||||
elif samples:
|
||||
with st.status("Starting style analysis...", expanded=True) as status:
|
||||
try:
|
||||
# Initialize style analyzer
|
||||
status.update(label="Analyzing content style...", state="running")
|
||||
style_analyzer = StyleAnalyzer()
|
||||
|
||||
# Perform style analysis
|
||||
style_analysis = style_analyzer.analyze_content_style({"main_content": samples})
|
||||
|
||||
if style_analysis.get('error'):
|
||||
status.update(label="Analysis failed", state="error")
|
||||
st.error(f"Style analysis failed: {style_analysis['error']}")
|
||||
else:
|
||||
status.update(label="Analysis complete!", state="complete")
|
||||
# Display style analysis results
|
||||
display_style_analysis(style_analysis)
|
||||
except Exception as e:
|
||||
status.update(label="Analysis failed", state="error")
|
||||
st.error(f"An error occurred during analysis: {str(e)}")
|
||||
else:
|
||||
st.warning("Please provide either a website URL or content samples to analyze.")
|
||||
|
||||
# Save Settings Button with premium styling
|
||||
st.markdown("<div style='height: 2rem'></div>", unsafe_allow_html=True)
|
||||
if st.button("💾 Save All Settings", type="primary", use_container_width=True, key="settings_save_button"):
|
||||
# Save all settings to session state
|
||||
st.session_state.update({
|
||||
'blog_length': blog_length,
|
||||
'blog_tone': blog_tone,
|
||||
'blog_demographic': blog_demographic,
|
||||
'blog_type': blog_type,
|
||||
'blog_language': blog_language,
|
||||
'blog_output_format': blog_output_format,
|
||||
'image_generation_model': image_generation_model,
|
||||
'number_of_blog_images': number_of_blog_images,
|
||||
'gpt_provider': gpt_provider,
|
||||
'model': model,
|
||||
'temperature': temperature,
|
||||
'top_p': top_p,
|
||||
'max_tokens': max_tokens,
|
||||
'frequency_penalty': frequency_penalty,
|
||||
'geographic_location': geographic_location,
|
||||
'search_language': search_language,
|
||||
'number_of_results': number_of_results,
|
||||
'time_range': time_range,
|
||||
'include_domains': include_domains,
|
||||
'similar_url': similar_url
|
||||
})
|
||||
st.success("✅ Settings saved successfully! Your preferences have been applied to all AI tools.")
|
||||
|
||||
# Show a summary of saved settings
|
||||
st.markdown("""
|
||||
<div class="analysis-card" style="margin-top: 1rem;">
|
||||
<h4 style="color: #ffffff; margin-bottom: 1rem;">📋 Settings Summary</h4>
|
||||
<div style="color: rgba(255, 255, 255, 0.9); line-height: 1.6;">
|
||||
<p><strong>Content:</strong> {length} words, {tone} tone, {audience} audience</p>
|
||||
<p><strong>Images:</strong> {images} images using {model}</p>
|
||||
<p><strong>AI Model:</strong> {provider} - {ai_model}</p>
|
||||
<p><strong>Search:</strong> {location} region, {results} results</p>
|
||||
</div>
|
||||
</div>
|
||||
""".format(
|
||||
length=blog_length,
|
||||
tone=blog_tone,
|
||||
audience=blog_demographic,
|
||||
images=number_of_blog_images,
|
||||
model=image_generation_model,
|
||||
provider=gpt_provider,
|
||||
ai_model=model,
|
||||
location=geographic_location,
|
||||
results=number_of_results
|
||||
), unsafe_allow_html=True)
|
||||
374
ToBeMigrated/alwrity_ui/similar_analysis.py
Normal file
374
ToBeMigrated/alwrity_ui/similar_analysis.py
Normal file
@@ -0,0 +1,374 @@
|
||||
import streamlit as st
|
||||
from lib.ai_web_researcher.metaphor_basic_neural_web_search import metaphor_find_similar
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
import urllib.parse
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Create console handler if it doesn't exist
|
||||
if not logger.handlers:
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
def is_valid_url(url):
|
||||
"""
|
||||
Check if the provided string is a valid URL.
|
||||
|
||||
Args:
|
||||
url (str): The URL to validate
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
try:
|
||||
result = urllib.parse.urlparse(url)
|
||||
is_valid = all([result.scheme, result.netloc])
|
||||
logger.debug(f"URL validation for {url}: {is_valid}")
|
||||
return is_valid
|
||||
except Exception as e:
|
||||
logger.error(f"URL validation error for {url}: {str(e)}")
|
||||
return False
|
||||
|
||||
def competitor_analysis():
|
||||
logger.info("Starting competitor analysis")
|
||||
|
||||
# Initialize session state for progress bar visibility
|
||||
if 'show_progress' not in st.session_state:
|
||||
st.session_state.show_progress = True
|
||||
logger.debug("Initialized show_progress session state")
|
||||
|
||||
st.title("Competitor Analysis")
|
||||
st.markdown("""**Use Cases:**
|
||||
- Know similar companies and alternatives for the given URL.
|
||||
- Write listicles, similar companies, Top tools, alternative-to, similar products, similar websites, etc.
|
||||
[Read More Here](https://docs.exa.ai/reference/company-analyst)
|
||||
""")
|
||||
|
||||
# URL input with validation
|
||||
similar_url = st.text_input(
|
||||
"👋 Enter a single valid URL for web analysis:",
|
||||
placeholder="https://example.com",
|
||||
help="Enter a complete URL including http:// or https://"
|
||||
)
|
||||
|
||||
# Validate URL
|
||||
url_valid = is_valid_url(similar_url) if similar_url else False
|
||||
if similar_url and not url_valid:
|
||||
logger.warning(f"Invalid URL provided: {similar_url}")
|
||||
st.error("⚠️ Please enter a valid URL including http:// or https://")
|
||||
|
||||
# Usecase selection with improved help
|
||||
usecase = st.selectbox(
|
||||
"Select Usecase",
|
||||
["similar companies", "listicles", "Top tools", "alternative-to", "similar products", "similar websites"],
|
||||
help="Choose the type of analysis you want to perform"
|
||||
)
|
||||
logger.debug(f"Selected usecase: {usecase}")
|
||||
|
||||
# Default summary query based on usecase
|
||||
default_summary_queries = {
|
||||
"similar companies": "Find companies similar to this one, focusing on their business model, target audience, and market position",
|
||||
"listicles": "Find similar listicle articles about this topic, focusing on the structure and content",
|
||||
"Top tools": "Find top tools similar to this one, focusing on features, pricing, and user reviews",
|
||||
"alternative-to": "Find alternatives to this product or service, focusing on comparable features and pricing",
|
||||
"similar products": "Find products similar to this one, focusing on features, specifications, and use cases",
|
||||
"similar websites": "Find websites similar to this one, focusing on design, content, and functionality"
|
||||
}
|
||||
|
||||
# Advanced options using a modal dialog
|
||||
show_advanced = st.checkbox("Show Advanced Options", help="Configure additional search parameters")
|
||||
logger.debug(f"Advanced options shown: {show_advanced}")
|
||||
|
||||
# Initialize default values
|
||||
num_results = 5
|
||||
time_range = "Anytime"
|
||||
include_domains = []
|
||||
exclude_domains = []
|
||||
include_text = None
|
||||
exclude_text = None
|
||||
summary_query = default_summary_queries.get(usecase, "")
|
||||
|
||||
# Add custom CSS for card styling
|
||||
st.markdown("""
|
||||
<style>
|
||||
.card {
|
||||
background: linear-gradient(to right, #f0f8ff, #e6f3ff);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
border-left: 4px solid #4a90e2;
|
||||
}
|
||||
.card-title {
|
||||
color: #2c5282;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Advanced options section
|
||||
if show_advanced:
|
||||
logger.debug("Processing advanced options")
|
||||
st.markdown("### 🔧 Advanced Search Options")
|
||||
|
||||
# Summary query with improved help in a card
|
||||
st.markdown('<div class="card"><div class="card-title">📝 Summary Query</div></div>', unsafe_allow_html=True)
|
||||
summary_query = st.text_area(
|
||||
"Customize the summary query",
|
||||
value=summary_query,
|
||||
placeholder="Enter a custom query for summarization based on your usecase",
|
||||
help="This query will be used to generate summaries of the similar content. Be specific about what you want to know."
|
||||
)
|
||||
|
||||
# Number of results with improved help in a card
|
||||
st.markdown('<div class="card"><div class="card-title">🔢 Number of Results</div></div>', unsafe_allow_html=True)
|
||||
num_results = st.slider(
|
||||
"How many results would you like?",
|
||||
min_value=1,
|
||||
max_value=20,
|
||||
value=5,
|
||||
step=1,
|
||||
help="How many similar results would you like to see?"
|
||||
)
|
||||
|
||||
# Progress bar visibility toggle
|
||||
st.markdown('<div class="card"><div class="card-title">🔄 Progress Display</div></div>', unsafe_allow_html=True)
|
||||
st.session_state.show_progress = st.toggle(
|
||||
"Show detailed progress bars",
|
||||
value=st.session_state.show_progress,
|
||||
help="Toggle to show or hide detailed progress bars during analysis"
|
||||
)
|
||||
|
||||
# Time range selection with improved styling in a card
|
||||
st.markdown('<div class="card"><div class="card-title">⏱️ Time Range</div></div>', unsafe_allow_html=True)
|
||||
time_range = st.radio(
|
||||
"Select time range for results",
|
||||
options=["Past Week", "Past Month", "Past Year", "Anytime"],
|
||||
index=3,
|
||||
horizontal=True,
|
||||
help="Filter results by when they were published"
|
||||
)
|
||||
|
||||
# Domain filters with improved styling in a card
|
||||
st.markdown('<div class="card"><div class="card-title">🌐 Domain Filters</div></div>', unsafe_allow_html=True)
|
||||
domain_filter_type = st.radio(
|
||||
"Domain Filter Type",
|
||||
options=["Include Domains", "Exclude Domains", "None"],
|
||||
index=2,
|
||||
horizontal=True,
|
||||
help="Include or exclude specific domains from search results"
|
||||
)
|
||||
|
||||
if domain_filter_type == "Include Domains":
|
||||
include_domains_input = st.text_input(
|
||||
"Include Domains (comma-separated)",
|
||||
placeholder="example.com, another-example.com",
|
||||
help="Only results from these domains will be included. Example: arxiv.org, paperswithcode.com"
|
||||
)
|
||||
if include_domains_input:
|
||||
include_domains = [domain.strip() for domain in include_domains_input.split(",")]
|
||||
|
||||
elif domain_filter_type == "Exclude Domains":
|
||||
exclude_domains_input = st.text_input(
|
||||
"Exclude Domains (comma-separated)",
|
||||
placeholder="example.com, another-example.com",
|
||||
help="Results from these domains will be excluded from search results"
|
||||
)
|
||||
if exclude_domains_input:
|
||||
exclude_domains = [domain.strip() for domain in exclude_domains_input.split(",")]
|
||||
|
||||
# Text filters with improved styling in a card
|
||||
st.markdown('<div class="card"><div class="card-title">📝 Text Filters</div></div>', unsafe_allow_html=True)
|
||||
text_filter_type = st.radio(
|
||||
"Text Filter Type",
|
||||
options=["Include Text", "Exclude Text", "None"],
|
||||
index=2,
|
||||
horizontal=True,
|
||||
help="Include or exclude results containing specific text"
|
||||
)
|
||||
|
||||
if text_filter_type == "Include Text":
|
||||
include_text = st.text_input(
|
||||
"Include Text",
|
||||
placeholder="large language model",
|
||||
help="Only results containing this phrase will be included (up to 5 words)"
|
||||
)
|
||||
|
||||
elif text_filter_type == "Exclude Text":
|
||||
exclude_text = st.text_input(
|
||||
"Exclude Text",
|
||||
placeholder="course",
|
||||
help="Results containing this phrase will be excluded (up to 5 words)"
|
||||
)
|
||||
|
||||
# Analyze button with validation
|
||||
if st.button("Analyze", disabled=not url_valid if similar_url else False):
|
||||
if similar_url and url_valid:
|
||||
try:
|
||||
logger.info(f"Starting analysis for URL: {similar_url}")
|
||||
logger.debug(f"Analysis parameters - Usecase: {usecase}, Num Results: {num_results}, Time Range: {time_range}")
|
||||
|
||||
# Create a progress container
|
||||
progress_container = st.empty()
|
||||
status_container = st.empty()
|
||||
results_container = st.empty()
|
||||
|
||||
# Display initial status
|
||||
status_container.info(f"Starting analysis for the URL: {similar_url}")
|
||||
|
||||
# Create a progress bar
|
||||
progress_bar = progress_container.progress(0.1)
|
||||
logger.debug("Initialized progress bar and status containers")
|
||||
|
||||
# Calculate date range based on selection
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
if time_range != "Anytime":
|
||||
end_date = datetime.now()
|
||||
if time_range == "Past Week":
|
||||
start_date = end_date - timedelta(days=7)
|
||||
elif time_range == "Past Month":
|
||||
start_date = end_date - timedelta(days=30)
|
||||
elif time_range == "Past Year":
|
||||
start_date = end_date - timedelta(days=365)
|
||||
logger.debug(f"Date range: {start_date} to {end_date}")
|
||||
|
||||
# Format dates for API if they exist
|
||||
start_published_date = start_date.strftime("%Y-%m-%dT%H:%M:%S.000Z") if start_date else None
|
||||
end_published_date = end_date.strftime("%Y-%m-%dT%H:%M:%S.999Z") if end_date else None
|
||||
|
||||
# Prepare summary query
|
||||
summary_query_param = None
|
||||
if summary_query:
|
||||
summary_query_param = {"query": summary_query}
|
||||
logger.debug(f"Summary query: {summary_query}")
|
||||
|
||||
# Update progress
|
||||
progress_bar.progress(0.2)
|
||||
status_container.info("Searching for similar content...")
|
||||
logger.info("Initiating similar content search")
|
||||
|
||||
# Call the metaphor_find_similar function with all parameters
|
||||
with st.spinner("Performing competitor analysis..."):
|
||||
logger.debug("Calling metaphor_find_similar API")
|
||||
try:
|
||||
df, search_response = metaphor_find_similar(
|
||||
similar_url=similar_url,
|
||||
usecase=usecase,
|
||||
num_results=num_results,
|
||||
start_published_date=start_published_date,
|
||||
end_published_date=end_published_date,
|
||||
include_domains=include_domains,
|
||||
exclude_domains=exclude_domains,
|
||||
include_text=include_text,
|
||||
exclude_text=exclude_text,
|
||||
summary_query=summary_query_param
|
||||
)
|
||||
logger.info(f"API call successful. Found {len(df) if not df.empty else 0} results")
|
||||
|
||||
# Update progress
|
||||
progress_bar.progress(0.7)
|
||||
status_container.info("Processing and analyzing results...")
|
||||
logger.debug("Processing search results")
|
||||
|
||||
# Update progress to complete
|
||||
progress_bar.progress(1.0)
|
||||
status_container.success("Analysis completed successfully!")
|
||||
logger.info("Analysis completed successfully")
|
||||
|
||||
except Exception as api_error:
|
||||
logger.error(f"API call failed: {str(api_error)}", exc_info=True)
|
||||
raise
|
||||
|
||||
# Display results
|
||||
if not df.empty:
|
||||
logger.debug(f"Displaying {len(df)} results")
|
||||
st.subheader("📊 Competitor Analysis Results")
|
||||
|
||||
# Add a download button for the results
|
||||
csv = df.to_csv(index=False)
|
||||
st.download_button(
|
||||
label="📥 Download Results as CSV",
|
||||
data=csv,
|
||||
file_name=f"competitor_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
|
||||
mime="text/csv",
|
||||
)
|
||||
|
||||
# Display the data editor
|
||||
st.data_editor(
|
||||
df,
|
||||
column_config={
|
||||
"Title": st.column_config.TextColumn(
|
||||
"Title",
|
||||
help="Title of the similar content",
|
||||
width="large",
|
||||
),
|
||||
"URL": st.column_config.LinkColumn(
|
||||
"URL",
|
||||
help="Link to the similar content",
|
||||
width="medium",
|
||||
display_text="Visit Website",
|
||||
),
|
||||
"Content Summary": st.column_config.TextColumn(
|
||||
"Content Summary",
|
||||
help="Summary of the similar content",
|
||||
width="large",
|
||||
),
|
||||
},
|
||||
hide_index=True,
|
||||
use_container_width=True,
|
||||
)
|
||||
|
||||
# Display additional insights
|
||||
st.subheader("🔍 Analysis Insights")
|
||||
|
||||
# Create columns for metrics
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric("Total Results", len(df))
|
||||
|
||||
with col2:
|
||||
# Calculate average content length
|
||||
avg_content_length = df["Content Summary"].str.len().mean()
|
||||
st.metric("Avg. Content Length", f"{avg_content_length:.0f} chars")
|
||||
|
||||
with col3:
|
||||
# Calculate unique domains
|
||||
unique_domains = len(set([url.split('/')[2] for url in df["URL"]]))
|
||||
st.metric("Unique Domains", unique_domains)
|
||||
|
||||
# Display full summaries in expanders
|
||||
st.subheader("📝 Detailed Competitor Summaries")
|
||||
|
||||
if 'competitor_summaries' in st.session_state and st.session_state.competitor_summaries:
|
||||
for url, data in st.session_state.competitor_summaries.items():
|
||||
with st.expander(f"📊 {data['title']}", expanded=False):
|
||||
st.markdown("### 📝 Detailed Competitor Analysis")
|
||||
st.markdown(data['summary'])
|
||||
|
||||
# Display raw data in an expander
|
||||
with st.expander("View Raw Data"):
|
||||
st.json(search_response)
|
||||
else:
|
||||
logger.warning("No results found for the given URL and parameters")
|
||||
st.warning("No results found for the given URL and parameters.")
|
||||
|
||||
except Exception as err:
|
||||
logger.error(f"Analysis failed: {str(err)}", exc_info=True)
|
||||
st.error(f"✖ 🚫 Failed to do similar search.\nError: {err}")
|
||||
else:
|
||||
logger.warning("Analysis attempted without valid URL")
|
||||
st.error("Please enter a valid URL.")
|
||||
116
ToBeMigrated/alwrity_ui/social_media_dashboard.py
Normal file
116
ToBeMigrated/alwrity_ui/social_media_dashboard.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import streamlit as st
|
||||
from lib.alwrity_ui.dashboard_styles import apply_dashboard_style, render_dashboard_header, render_card
|
||||
from loguru import logger
|
||||
|
||||
def render_social_tools_dashboard():
|
||||
"""Render the social media tools dashboard with premium glassmorphic design."""
|
||||
logger.info("Starting Social Media Tools Dashboard")
|
||||
|
||||
# Apply common dashboard styling
|
||||
apply_dashboard_style()
|
||||
|
||||
# Render dashboard header
|
||||
render_dashboard_header(
|
||||
"📱 AI Social Media Tools",
|
||||
"Create engaging social media content across all major platforms with our specialized AI writers. From viral posts to professional updates, we've got you covered."
|
||||
)
|
||||
|
||||
# Define social tools with enhanced details and platform-specific styling
|
||||
social_tools = {
|
||||
"Facebook": {
|
||||
"icon": "📘",
|
||||
"description": "Create engaging Facebook posts, stories, and ads that drive meaningful interactions and build community",
|
||||
"category": "Social Network",
|
||||
"path": "facebook",
|
||||
"features": ["Post Generation", "Story Creation", "Ad Copy", "Community Management"]
|
||||
},
|
||||
"LinkedIn": {
|
||||
"icon": "💼",
|
||||
"description": "Generate professional LinkedIn content, articles, and networking posts that enhance your career presence",
|
||||
"category": "Professional",
|
||||
"path": "linkedin",
|
||||
"features": ["Professional Posts", "Article Writing", "Network Building", "Career Content"]
|
||||
},
|
||||
"Twitter": {
|
||||
"icon": "🐦",
|
||||
"description": "Craft viral tweets, threads, and engaging content that sparks conversations and grows your following",
|
||||
"category": "Microblogging",
|
||||
"path": "twitter",
|
||||
"features": ["Tweet Generation", "Thread Creation", "Hashtag Strategy", "Viral Content"]
|
||||
},
|
||||
"Instagram": {
|
||||
"icon": "📸",
|
||||
"description": "Create captivating Instagram captions, stories, and content that showcases your brand beautifully",
|
||||
"category": "Visual Content",
|
||||
"path": "instagram",
|
||||
"features": ["Caption Writing", "Story Content", "Hashtag Research", "Visual Strategy"]
|
||||
},
|
||||
"YouTube": {
|
||||
"icon": "🎥",
|
||||
"description": "Generate compelling video scripts, descriptions, and content strategies for your YouTube channel",
|
||||
"category": "Video Content",
|
||||
"path": "youtube",
|
||||
"features": ["Script Writing", "Video Descriptions", "SEO Optimization", "Content Planning"]
|
||||
}
|
||||
}
|
||||
|
||||
# Create a responsive grid of premium cards
|
||||
cols = st.columns(3)
|
||||
for idx, (platform, details) in enumerate(social_tools.items()):
|
||||
with cols[idx % 3]:
|
||||
# Use the common card renderer
|
||||
if render_card(
|
||||
icon=details['icon'],
|
||||
title=platform,
|
||||
description=details['description'],
|
||||
category=details['category'],
|
||||
key_suffix=f"social_{platform}",
|
||||
help_text=f"Open {platform} content creation tools - {details['description'][:50]}..."
|
||||
):
|
||||
# Set query parameters to redirect to the specific tool
|
||||
st.query_params["tool"] = details["path"]
|
||||
st.rerun()
|
||||
|
||||
# Add feature showcase section
|
||||
st.markdown("""
|
||||
<div style="margin-top: 3rem;">
|
||||
<div class="dashboard-header" style="margin-bottom: 2rem;">
|
||||
<h1 style="font-size: 2.2em;">✨ Platform Features</h1>
|
||||
<p>Each platform comes with specialized AI tools designed for optimal engagement and growth.</p>
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Feature grid
|
||||
feature_cols = st.columns(2)
|
||||
features = [
|
||||
{
|
||||
"title": "🎯 Smart Content Generation",
|
||||
"description": "AI-powered content creation tailored to each platform's unique audience and format requirements."
|
||||
},
|
||||
{
|
||||
"title": "📊 Engagement Optimization",
|
||||
"description": "Data-driven insights and suggestions to maximize likes, shares, comments, and overall engagement."
|
||||
},
|
||||
{
|
||||
"title": "🕒 Optimal Timing",
|
||||
"description": "AI recommendations for the best times to post based on your audience's activity patterns."
|
||||
},
|
||||
{
|
||||
"title": "🔍 Hashtag Intelligence",
|
||||
"description": "Smart hashtag suggestions and trending topic analysis to increase your content's discoverability."
|
||||
}
|
||||
]
|
||||
|
||||
for idx, feature in enumerate(features):
|
||||
with feature_cols[idx % 2]:
|
||||
st.markdown(f"""
|
||||
<div class="premium-card" style="min-height: 160px; cursor: default;">
|
||||
<div class="card-glow"></div>
|
||||
<div class="card-content">
|
||||
<div class="card-title" style="margin-bottom: 0.8rem;">{feature['title']}</div>
|
||||
<div class="card-description" style="margin-bottom: 0;">{feature['description']}</div>
|
||||
</div>
|
||||
<div class="card-shine"></div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
Reference in New Issue
Block a user