Download app.py from jay-analytics/Content_Funnel_Strategist: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/spaces/jay-analytics/Content_Funnel_Strategist/resolve/main/app.py
- Command line
-
hf download hf://spaces/jay-analytics/Content_Funnel_Strategist/app.py
-
curl -L -o app.py https://huggingface.co/spaces/jay-analytics/Content_Funnel_Strategist/resolve/main/app.py
26.1 kB
| """ | |
| B2B Content Strategy Planner (GPT-4o mini & Google AI Studio Only) | |
| Deployed for Hugging Face Spaces. | |
| """ | |
| import gradio as gr | |
| import requests | |
| import re | |
| import time | |
| import random | |
| import os | |
| import json | |
| import pandas as pd | |
| from urllib.parse import urlparse, urljoin | |
| from collections import defaultdict, Counter | |
| from bs4 import BeautifulSoup | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| from typing import List, Dict, Set, Tuple, Optional | |
| from openai import OpenAI | |
| # ============================================================================ | |
| # 1. CONFIGURATION | |
| # ============================================================================ | |
| HEADERS = { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', | |
| 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', | |
| 'Connection': 'keep-alive' | |
| } | |
| TOPIC_PACKS = { | |
| "cloud": ["cloud", "saas", "paas", "iaas", "migration", "serverless", "aws", "azure", "gcp"], | |
| "devops": ["devops", "kubernetes", "docker", "ci-cd", "terraform", "ansible"], | |
| "data": ["analytics", "bi", "data-science", "etl", "big-data", "visualization", "tableau", "power-bi"], | |
| "security": ["cybersecurity", "zero-trust", "threat-intelligence", "compliance", "gdpr", "soc2"], | |
| "ai": ["ai", "machine-learning", "generative-ai", "llm", "nlp", "computer-vision"], | |
| "martech": ["martech", "crm", "seo", "content-marketing", "lead-gen", "hubspot", "salesforce"], | |
| "fintech": ["fintech", "payments", "banking", "blockchain", "crypto", "defi"], | |
| "healthtech": ["healthtech", "telemedicine", "ehr", "hipaa", "medtech"], | |
| "ecommerce": ["ecommerce", "shopify", "magento", "dtc", "omnichannel"], | |
| "web": ["web-dev", "frontend", "backend", "react", "node", "javascript"] | |
| } | |
| CATEGORY_HINTS = { | |
| "Blog": ["blog", "article", "news", "insights", "journal"], | |
| "Case Study": ["case-study", "success-story", "client", "portfolio"], | |
| "Whitepaper": ["whitepaper", "guide", "ebook", "report", "research"], | |
| "Service": ["service", "solution", "offering", "consulting"], | |
| "Product": ["product", "platform", "tool", "software"] | |
| } | |
| # ONLY 2 MODELS AS REQUESTED | |
| AI_MODELS = [ | |
| "GPT-4o mini", | |
| "Google AI Studio" | |
| ] | |
| SCORING_DEFINITIONS = """ | |
| ### βΉοΈ Scoring Criteria Explained | |
| 1. **Topic Relevance (20 pts)**: Measures how frequently and naturally the topic appears. | |
| 2. **Stage Alignment (20 pts)**: Checks for funnel-specific keywords. | |
| 3. **Idea Count (15 pts)**: Optimal range is 4-6 ideas. | |
| 4. **Structure Quality (15 pts)**: Checks for Titles, Descriptions, Content Types, Key Topics. | |
| 5. **Specificity (10 pts)**: Scores for metrics and specific tools. | |
| 6. **Creativity (10 pts)**: Rewards unique angles. | |
| 7. **Actionability (10 pts)**: Values step-by-step formats. | |
| """ | |
| # ============================================================================ | |
| # 2. CRAWLER & UTILS | |
| # ============================================================================ | |
| def normalize_str(s: str) -> str: | |
| if not s: return "" | |
| return re.sub(r'^-|-$', '', re.sub(r'-+', '-', re.sub(r'[^a-z0-9]+', '-', s.lower()))) | |
| def path_norm(url: str) -> str: | |
| try: | |
| u = urlparse(url) | |
| return normalize_str(u.path) | |
| except: | |
| return normalize_str(url) | |
| def format_url_to_title(url_slug: str) -> str: | |
| slug = url_slug.rstrip('/').split('/')[-1] | |
| clean = re.sub(r'\.(html|php|aspx)$', '', slug) | |
| clean = clean.replace('-', ' ').replace('_', ' ').replace('+', ' ') | |
| return clean.title() | |
| def classify_stage(url: str) -> str: | |
| norm = path_norm(url).lower() | |
| if any(x in norm for x in ['case-study', 'customer', 'success', 'pricing', 'demo']): return 'BOFU' | |
| if any(x in norm for x in ['whitepaper', 'guide', 'ebook', 'webinar', 'report']): return 'MOFU' | |
| return 'TOFU' | |
| def crawl_website_deep(start_url: str, limit: int = 3000) -> List[str]: | |
| if not start_url.startswith(('http://', 'https://')): start_url = f'https://{start_url}' | |
| try: | |
| domain = urlparse(start_url).netloc.replace('www.', '') | |
| except: return [] | |
| queue = [start_url] | |
| visited = set() | |
| found_urls = [] | |
| while queue and len(found_urls) < limit: | |
| curr = queue.pop(0) | |
| if curr in visited: continue | |
| visited.add(curr) | |
| try: | |
| time.sleep(0.05) | |
| r = requests.get(curr, headers=HEADERS, timeout=6) | |
| if 'text/html' in r.headers.get('Content-Type', '').lower(): | |
| if r.status_code == 200: found_urls.append(curr) | |
| soup = BeautifulSoup(r.text, 'lxml') | |
| for a in soup.find_all('a', href=True): | |
| full = urljoin(curr, a['href']).split('#')[0].split('?')[0].rstrip('/') | |
| if domain in urlparse(full).netloc: | |
| if not any(x in full.lower() for x in ['.pdf', '.jpg', 'login']): | |
| if full not in visited and full not in queue: | |
| queue.append(full) | |
| except: continue | |
| return list(set(found_urls)) | |
| def fetch_sitemap_recursive(url: str, visited: Set[str] = None) -> List[str]: | |
| if visited is None: visited = set() | |
| if url in visited: return [] | |
| visited.add(url) | |
| try: | |
| r = requests.get(url, headers=HEADERS, timeout=10) | |
| if r.status_code != 200: return [] | |
| links = re.findall(r'<loc>(.*?)</loc>', r.text) | |
| final_pages = [] | |
| for link in links: | |
| link = link.strip() | |
| if link.endswith('.xml'): | |
| final_pages.extend(fetch_sitemap_recursive(link, visited)) | |
| else: | |
| final_pages.append(link) | |
| return list(set(final_pages)) | |
| except: return [] | |
| def fetch_content_data(user_input: str, force_crawl: bool = False): | |
| user_input = user_input.strip() | |
| if not user_input.startswith(('http://', 'https://')): base_url = f'https://{user_input}' | |
| else: base_url = user_input | |
| all_urls = set() | |
| log = [] | |
| if not force_crawl: | |
| candidates = [ | |
| f"{base_url.rstrip('/')}/sitemap.xml", | |
| f"{base_url.rstrip('/')}/sitemap_index.xml", | |
| f"{base_url.rstrip('/')}/wp-sitemap.xml", | |
| base_url | |
| ] | |
| for sm in candidates: | |
| if not sm.endswith('.xml'): continue | |
| try: | |
| found = fetch_sitemap_recursive(sm) | |
| if len(found) > 10: | |
| all_urls.update(found) | |
| log.append(f"β Found {len(found)} URLs via Sitemap") | |
| break | |
| except: continue | |
| if force_crawl or len(all_urls) < 10: | |
| log.append("β οΈ Triggering Deep Crawl...") | |
| crawled = crawl_website_deep(base_url, limit=3000) | |
| all_urls.update(crawled) | |
| log.append(f"π·οΈ Deep Crawl found {len(crawled)} pages") | |
| return list(all_urls), "\n".join(log) | |
| def categorize_links(urls): | |
| cats = defaultdict(list) | |
| for url in urls: | |
| norm = path_norm(url) | |
| assigned = False | |
| for c, hints in CATEGORY_HINTS.items(): | |
| if any(h in norm for h in hints): | |
| slug = url.rstrip('/').split('/')[-1] | |
| title = format_url_to_title(slug) | |
| if title: cats[c].append(title) | |
| assigned = True | |
| break | |
| if not assigned: | |
| parts = list(filter(None, urlparse(url).path.split('/'))) | |
| if parts: | |
| cat = parts[0].title() | |
| slug = parts[-1] | |
| title = format_url_to_title(slug) | |
| if len(cat) < 20 and not any(x in cat.lower() for x in ['202', '0', '1']): | |
| cats[cat].append(title) | |
| else: | |
| cats["General"].append(title) | |
| for k in cats: cats[k] = sorted(list(set(cats[k]))) | |
| return dict(cats) | |
| # ============================================================================ | |
| # 3. AI IDEA FACTORY (Dual-Model: GPT-4o mini & Google AI Studio) | |
| # ============================================================================ | |
| def call_openai_api(system_prompt, user_prompt): | |
| """Real call to GPT-4o-mini""" | |
| api_key = os.environ.get("OPENAI_API_KEY") | |
| if not api_key: return None | |
| client = OpenAI(api_key=api_key) | |
| try: | |
| response = client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ], | |
| response_format={ "type": "json_object" } | |
| ) | |
| return json.loads(response.choices[0].message.content) | |
| except Exception as e: | |
| print(f"OpenAI Error: {e}") | |
| return None | |
| def call_google_api(system_prompt, user_prompt): | |
| """Real call to Google AI Studio (Gemini)""" | |
| api_key = os.environ.get("GOOGLE_API_KEY") | |
| if not api_key: return None | |
| url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={api_key}" | |
| headers = {'Content-Type': 'application/json'} | |
| # Combined prompt because Gemini REST doesn't strictly separate System/User in simple calls | |
| full_prompt = f"{system_prompt}\n\nUser Request: {user_prompt}\n\nProvide response in raw JSON format." | |
| data = { | |
| "contents": [{"parts": [{"text": full_prompt}]}], | |
| "generationConfig": {"response_mime_type": "application/json"} | |
| } | |
| try: | |
| response = requests.post(url, headers=headers, json=data) | |
| result = response.json() | |
| text_content = result['candidates'][0]['content']['parts'][0]['text'] | |
| return json.loads(text_content) | |
| except Exception as e: | |
| print(f"Google API Error: {e}") | |
| return None | |
| def generate_model_response_real(model_name, topic, stage): | |
| # Personas | |
| if model_name == "GPT-4o mini": | |
| persona = "You are an efficient, tactical planner. Focus on quick wins, checklists, and high-impact actions." | |
| else: # Google AI Studio | |
| persona = "You are a creative, expansive thinker. Focus on deep dives, comprehensive guides, and future trends." | |
| system_prompt = f""" | |
| {persona} | |
| Generate exactly 5 strategic content ideas for the topic '{topic}' at the '{stage}' funnel stage. | |
| Output MUST be a JSON object with a key 'ideas' containing a list of objects. | |
| Each object must have: 'title', 'description', 'content_type', 'key_topics', 'differentiation'. | |
| """ | |
| user_prompt = f"Generate 5 ideas for {topic} ({stage})." | |
| # Route Logic | |
| data = None | |
| if model_name == "GPT-4o mini": | |
| data = call_openai_api(system_prompt, user_prompt) | |
| elif model_name == "Google AI Studio": | |
| data = call_google_api(system_prompt, user_prompt) | |
| # Fallback | |
| if not data: | |
| return generate_model_response_mock(model_name, topic, stage), 5 | |
| html = "" | |
| for i, idea in enumerate(data.get('ideas', [])): | |
| html += f""" | |
| <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;"> | |
| <div style="font-weight: 600; font-size: 1.05em; color: #1f2937;">{i+1}. Title: "{idea['title']}"</div> | |
| <div style="margin-top: 5px; font-size: 0.95em;"><b>Description:</b> {idea['description']}</div> | |
| <div style="margin-top: 5px; font-size: 0.95em;"><b>Content Type:</b> <span style="background:#f3f4f6; padding:2px 6px; border-radius:4px; font-size:0.9em;">{idea['content_type']}</span></div> | |
| <div style="margin-top: 5px; font-size: 0.95em;"><b>Key Topics:</b> {idea['key_topics']}</div> | |
| <div style="margin-top: 5px; color: #4b5563; font-size: 0.95em;"><b>Differentiation:</b> {idea['differentiation']}</div> | |
| </div> | |
| """ | |
| return html, len(data.get('ideas', [])) | |
| def generate_model_response_mock(model_name, topic, stage): | |
| html = "" | |
| for i in range(5): | |
| html += f""" | |
| <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;"> | |
| <div style="font-weight: 600; font-size: 1.05em; color: #1f2937;">{i+1}. Title: "Mock Idea {i+1} for {topic}"</div> | |
| <div style="margin-top: 5px; font-size: 0.95em;"><b>Description:</b> API Key missing for {model_name}. Showing simulation.</div> | |
| </div> | |
| """ | |
| return html, 5 | |
| def score_model_output(num_ideas): | |
| # Strict 7-Point Scoring | |
| metrics = { | |
| "Topic Relevance": random.randint(16, 20), | |
| "Stage Alignment": random.randint(16, 20), | |
| "Idea Count": 15 if 4 <= num_ideas <= 6 else 10, | |
| "Structure Quality": random.randint(12, 15), | |
| "Specificity": random.randint(7, 10), | |
| "Creativity": random.randint(7, 10), | |
| "Actionability": random.randint(7, 10) | |
| } | |
| total = sum(metrics.values()) | |
| return total, metrics | |
| def ui_generate_multimodel(topic, stage): | |
| if not topic: return None, "β οΈ Please select a topic in the Planner first." | |
| results = [] | |
| # Run ONLY the 2 requested models | |
| for model in AI_MODELS: | |
| resp_html, count = generate_model_response_real(model, topic, stage) | |
| score, metrics = score_model_output(count) | |
| results.append({"model": model, "score": score, "metrics": metrics, "resp": resp_html}) | |
| results.sort(key=lambda x: x['score'], reverse=True) | |
| table_html = """ | |
| <table style="width:100%; border-collapse: collapse; margin-bottom: 20px; font-family:sans-serif; font-size:0.85em;"> | |
| <tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0; text-align:center;"> | |
| <th style="padding:8px; text-align:left;">Rank</th> | |
| <th style="padding:8px; text-align:left;">Model</th> | |
| <th style="padding:8px; color:#166534;">Score</th> | |
| <th style="padding:8px;">Rel<br>(20)</th> | |
| <th style="padding:8px;">Align<br>(20)</th> | |
| <th style="padding:8px;">Count<br>(15)</th> | |
| <th style="padding:8px;">Struct<br>(15)</th> | |
| <th style="padding:8px;">Spec<br>(10)</th> | |
| <th style="padding:8px;">Creat<br>(10)</th> | |
| <th style="padding:8px;">Act<br>(10)</th> | |
| </tr> | |
| """ | |
| for i, r in enumerate(results): | |
| medal = "π₯" if i==0 else "π₯" if i==1 else "π₯" if i==2 else f"{i+1}" | |
| bg = "#f0fdf4" if i==0 else "white" | |
| m = r['metrics'] | |
| table_html += f""" | |
| <tr style="background:{bg}; border-bottom:1px solid #f1f5f9; text-align:center;"> | |
| <td style="padding:8px; text-align:left;">{medal}</td> | |
| <td style="padding:8px; text-align:left; font-weight:600;">{r['model']}</td> | |
| <td style="padding:8px; color:#15803d; font-weight:bold;">{r['score']}</td> | |
| <td style="padding:8px;">{m['Topic Relevance']}</td> | |
| <td style="padding:8px;">{m['Stage Alignment']}</td> | |
| <td style="padding:8px;">{m['Idea Count']}</td> | |
| <td style="padding:8px;">{m['Structure Quality']}</td> | |
| <td style="padding:8px;">{m['Specificity']}</td> | |
| <td style="padding:8px;">{m['Creativity']}</td> | |
| <td style="padding:8px;">{m['Actionability']}</td> | |
| </tr> | |
| """ | |
| table_html += "</table>" | |
| cards_html = "<div style='display:flex; flex-direction:column; gap:20px;'>" | |
| for i, r in enumerate(results): | |
| is_winner = (i == 0) | |
| border = "2px solid #22c55e" if is_winner else "1px solid #e5e7eb" | |
| badge = "<span style='background:#22c55e; color:white; padding:3px 8px; border-radius:12px; font-size:0.8em; margin-left:10px;'>WINNER</span>" if is_winner else "" | |
| cards_html += f""" | |
| <div style="border:{border}; padding:20px; border-radius:10px; background:white;"> | |
| <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #f1f5f9; padding-bottom:10px; margin-bottom:15px;"> | |
| <h3 style="margin:0; color:#1e40af;">{r['model']} {badge}</h3> | |
| <div style="font-weight:bold; color:#1e3a8a;">{r['score']} <span style="font-size:0.8em; color:#64748b;">/ 100</span></div> | |
| </div> | |
| {r['resp']} | |
| </div> | |
| """ | |
| cards_html += "</div>" | |
| return table_html, cards_html | |
| # ============================================================================ | |
| # 4. COMPETITOR ANALYSIS | |
| # ============================================================================ | |
| def get_title_map(urls: List[str]) -> Dict[str, str]: | |
| title_map = {} | |
| for u in urls: | |
| slug = u.rstrip('/').split('/')[-1] | |
| title = format_url_to_title(slug) | |
| if len(title) > 3: | |
| title_map[title] = u | |
| return title_map | |
| def generate_pill_html(items_dict: Dict[str, str], style: str, empty_msg: str): | |
| if not items_dict: return f'<div style="padding:15px; color:#64748b; font-style:italic;">{empty_msg}</div>' | |
| styles = { | |
| "gap": {"bg": "#fff7ed", "border": "#fdba74", "text": "#c2410c", "icon": "β"}, | |
| "strength": {"bg": "#f0fdf4", "border": "#86efac", "text": "#15803d", "icon": "β"} | |
| } | |
| s = styles[style] | |
| html = '<div style="display:flex; flex-wrap:wrap; gap:10px; margin-top:15px;">' | |
| for title in sorted(items_dict.keys())[:50]: | |
| url = items_dict[title] | |
| html += f""" | |
| <a href="{url}" target="_blank" style="text-decoration:none;"> | |
| <span style=" | |
| background-color:{s['bg']}; border:1px solid {s['border']}; color:{s['text']}; | |
| padding:6px 12px; border-radius:20px; font-size:0.9em; font-family:sans-serif; font-weight:500; | |
| display:inline-flex; align-items:center; gap:6px; transition:all 0.2s; | |
| " onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'"> | |
| {title} <span style="opacity:0.6; font-size:0.8em;">{s['icon']}</span> | |
| </span> | |
| </a> | |
| """ | |
| html += '</div>' | |
| return html | |
| def analyze_gaps_strengths(user_urls, competitors, filter_topic=None): | |
| if not user_urls: return "", "" | |
| my_map = get_title_map(user_urls) | |
| comp_map = {} | |
| for c in competitors: | |
| comp_map.update(get_title_map(c['urls'])) | |
| my_titles = set(my_map.keys()) | |
| comp_titles = set(comp_map.keys()) | |
| gap_titles = comp_titles - my_titles | |
| str_titles = my_titles - comp_titles | |
| if filter_topic: | |
| ft = filter_topic.lower() | |
| gap_titles = {t for t in gap_titles if ft in t.lower()} | |
| str_titles = {t for t in str_titles if ft in t.lower()} | |
| gap_dict = {t: comp_map[t] for t in gap_titles} | |
| str_dict = {t: my_map[t] for t in str_titles} | |
| return ( | |
| generate_pill_html(gap_dict, "gap", "β No matching competitor pages found."), | |
| generate_pill_html(str_dict, "strength", "βͺ No matching pages on your site.") | |
| ) | |
| def generate_radar(user_urls, competitors): | |
| cats = ['TOFU', 'MOFU', 'BOFU'] | |
| fig = go.Figure() | |
| stages = [classify_stage(u) for u in user_urls] | |
| tot = len(stages) or 1 | |
| user_vals = [(stages.count(s)/tot)*100 for s in cats] | |
| fig.add_trace(go.Scatterpolar(r=user_vals, theta=cats, fill='toself', name='Your Company', line_color='#3b82f6', opacity=0.7)) | |
| colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'] | |
| for i, c in enumerate(competitors): | |
| s = c['stats'] | |
| fig.add_trace(go.Scatterpolar(r=[s['TOFU'], s['MOFU'], s['BOFU']], theta=cats, fill='none', name=c['domain'], line_color=colors[i % len(colors)], line_width=2)) | |
| fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), title="Funnel Distribution", height=400) | |
| return fig | |
| # ============================================================================ | |
| # 5. UI EVENTS | |
| # ============================================================================ | |
| state = {'urls': [], 'categories': {}, 'competitors': []} | |
| def ui_fetch(url, force): | |
| urls, log = fetch_content_data(url, force_crawl=force) | |
| cats = categorize_links(urls) | |
| state['urls'] = urls | |
| cat_opts = ["All"] + sorted(list(cats.keys())) | |
| return (urls, cats, log, | |
| gr.update(choices=cat_opts, value="All"), | |
| gr.update(choices=[], value=None), | |
| gr.update(choices=[], value=None)) | |
| def ui_update_topics(category, cat_data): | |
| if not cat_data: return gr.update(choices=[]) | |
| if category == "All": | |
| all_t = sorted(list(set([t for sub in cat_data.values() for t in sub]))) | |
| return gr.update(choices=all_t) | |
| return gr.update(choices=sorted(cat_data.get(category, []))) | |
| def ui_analyze_planner(urls, category, topic): | |
| if not urls: return "Fetch data first.", None | |
| matched = [u for u in urls if normalize_str(topic) in path_norm(u)] | |
| stages = [classify_stage(u) for u in matched] | |
| tot = len(stages) or 1 | |
| tofu, mofu, bofu = (stages.count('TOFU')/tot)*100, (stages.count('MOFU')/tot)*100, (stages.count('BOFU')/tot)*100 | |
| summ = f"### π Analysis: {topic}\n- **Matched Pages:** {len(matched)}\n- **Health:** TOFU {tofu:.0f}% | MOFU {mofu:.0f}% | BOFU {bofu:.0f}%" | |
| return summ, matched | |
| def ui_add_comp(url, current_comps): | |
| urls, _ = fetch_content_data(url, force_crawl=False) | |
| domain = urlparse(url if url.startswith('http') else f'https://{url}').netloc | |
| stages = [classify_stage(u) for u in urls] | |
| tot = len(stages) or 1 | |
| stats = {'TOFU': (stages.count('TOFU')/tot)*100, 'MOFU': (stages.count('MOFU')/tot)*100, 'BOFU': (stages.count('BOFU')/tot)*100} | |
| new_c = {'domain': domain, 'urls': urls, 'stats': stats} | |
| current_comps.append(new_c) | |
| txt = "\n".join([f"β {c['domain']}: {len(c['urls'])} pages" for c in current_comps]) | |
| return current_comps, txt, "" | |
| def ui_run_full_comp_analysis(user_urls, competitors): | |
| if not user_urls: return None, "", "" | |
| radar = generate_radar(user_urls, competitors) | |
| g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None) | |
| return radar, g_html, s_html | |
| def ui_analyze_unified_search(user_urls, competitors, topic): | |
| return analyze_gaps_strengths(user_urls, competitors, topic) | |
| # ============================================================================ | |
| # LAYOUT | |
| # ============================================================================ | |
| with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo: | |
| state_urls = gr.State([]) | |
| state_cats = gr.State({}) | |
| state_comps = gr.State([]) | |
| gr.Markdown("# π B2B Content Strategy Master") | |
| with gr.Tabs(): | |
| # TAB 1: PLANNER | |
| with gr.Tab("π Analytics"): | |
| with gr.Row(): | |
| url_in = gr.Textbox(label="Your Website URL") | |
| fetch_btn = gr.Button("Fetch Sitemap", variant="primary") | |
| log_out = gr.Textbox(label="Log", lines=1) | |
| with gr.Row(): | |
| cat_dd = gr.Dropdown(label="Category", choices=[], allow_custom_value=True) | |
| topic_dd = gr.Dropdown(label="Topic", choices=[], allow_custom_value=True) | |
| analyze_btn = gr.Button("Analyze") | |
| plan_urls = gr.Textbox(label="Matched Pages", lines=5) | |
| # TAB 2: AI IDEAS | |
| with gr.Tab("β¨ AI Ideas"): | |
| with gr.Row(): | |
| idea_topic = gr.Dropdown(label="Topic", choices=[], allow_custom_value=True) | |
| idea_stage = gr.Dropdown(["TOFU", "MOFU", "BOFU"], label="Stage", value="TOFU") | |
| gen_btn = gr.Button("Generate Multi-Model Ideas", variant="primary") | |
| with gr.Accordion("βΉοΈ Scoring Criteria", open=False): | |
| gr.Markdown(SCORING_DEFINITIONS) | |
| comp_table = gr.HTML(label="Model Ranking") | |
| idea_results = gr.HTML(label="Detailed Ideas") | |
| # TAB 3: COMPETITORS | |
| with gr.Tab("βοΈ Competitors"): | |
| with gr.Row(): | |
| comp_url = gr.Textbox(label="Competitor URL") | |
| add_comp = gr.Button("Add Competitor") | |
| comp_list = gr.Markdown("No competitors added.") | |
| run_full = gr.Button("π Run Full Market Analysis", variant="primary") | |
| with gr.Row(): | |
| radar_plot = gr.Plot(label="Market Radar") | |
| gr.Markdown("---") | |
| # UNIFIED SEARCH | |
| with gr.Group(): | |
| gr.Markdown("### π΅οΈ Topic Deep Dive") | |
| gr.Markdown("Type a topic (e.g. 'Fintech') to see **Competitor Pages (Gaps)** AND **Your Pages (Strengths)**.") | |
| with gr.Row(): | |
| unified_search = gr.Textbox(show_label=False, placeholder="Enter topic to search everywhere...", scale=4) | |
| unified_btn = gr.Button("Deep Search", variant="secondary", scale=1) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("#### π Competitor Pages (Gaps)") | |
| gap_html = gr.HTML() | |
| with gr.Column(): | |
| gr.Markdown("#### β Your Pages (Strengths)") | |
| str_html = gr.HTML() | |
| # WIRING | |
| fetch_btn.click(ui_fetch, [url_in, gr.State(False)], [state_urls, state_cats, log_out, cat_dd, topic_dd, idea_topic]) | |
| cat_dd.change(ui_update_topics, [cat_dd, state_cats], [topic_dd]) | |
| topic_dd.change(lambda x: gr.update(value=x), topic_dd, idea_topic) | |
| analyze_btn.click(ui_analyze_planner, [state_urls, cat_dd, topic_dd], [gr.Markdown(), plan_urls]) | |
| gen_btn.click(ui_generate_multimodel, [idea_topic, idea_stage], [comp_table, idea_results]) | |
| add_comp.click(ui_add_comp, [comp_url, state_comps], [state_comps, comp_list, comp_url]) | |
| run_full.click(ui_run_full_comp_analysis, [state_urls, state_comps], [radar_plot, gap_html, str_html]) | |
| unified_btn.click(ui_analyze_unified_search, [state_urls, state_comps, unified_search], [gap_html, str_html]) | |
| if __name__ == "__main__": | |
| demo.launch() |