| """ |
| 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 |
|
|
| |
| |
| |
|
|
| 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"] |
| } |
|
|
| |
| 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. |
| """ |
|
|
| |
| |
| |
|
|
| 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) |
|
|
| |
| |
| |
|
|
| 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'} |
| |
| |
| 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): |
| |
| |
| if model_name == "GPT-4o mini": |
| persona = "You are an efficient, tactical planner. Focus on quick wins, checklists, and high-impact actions." |
| else: |
| 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})." |
| |
| |
| 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) |
| |
| |
| 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): |
| |
| 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 = [] |
| |
| |
| 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 |
|
|
| |
| |
| |
|
|
| 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 |
|
|
| |
| |
| |
|
|
| 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) |
|
|
| |
| |
| |
|
|
| 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(): |
| |
| 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) |
|
|
| |
| 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") |
|
|
| |
| 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("---") |
| |
| |
| 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() |
|
|
| |
| 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() |