Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
B2B Content Strategy Planner (
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
|
| 6 |
import gradio as gr
|
|
@@ -16,7 +16,7 @@ import plotly.express as px
|
|
| 16 |
from typing import List, Dict, Set, Tuple, Optional
|
| 17 |
|
| 18 |
# ============================================================================
|
| 19 |
-
# 1. CONFIGURATION & TOPIC PACKS
|
| 20 |
# ============================================================================
|
| 21 |
|
| 22 |
TOPIC_PACKS = {
|
|
@@ -47,7 +47,7 @@ CATEGORY_HINTS = {
|
|
| 47 |
}
|
| 48 |
|
| 49 |
# ============================================================================
|
| 50 |
-
# 2.
|
| 51 |
# ============================================================================
|
| 52 |
|
| 53 |
def normalize_str(s: str) -> str:
|
|
@@ -63,14 +63,8 @@ def path_norm(url: str) -> str:
|
|
| 63 |
except:
|
| 64 |
return normalize_str(url)
|
| 65 |
|
| 66 |
-
def slugify_topic(topic_text: str) -> str:
|
| 67 |
-
return normalize_str(topic_text)
|
| 68 |
-
|
| 69 |
def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[List[str], str]:
|
| 70 |
-
"""
|
| 71 |
-
Your Crawler Logic: Crawls a website to generate a sitemap dynamically.
|
| 72 |
-
Increased limit to 150 for better analysis.
|
| 73 |
-
"""
|
| 74 |
if not start_url.startswith(('http://', 'https://')):
|
| 75 |
start_url = f'https://{start_url}'
|
| 76 |
|
|
@@ -90,8 +84,8 @@ def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[Li
|
|
| 90 |
while queue and len(found_urls) < max_pages:
|
| 91 |
current_url = queue.pop(0)
|
| 92 |
try:
|
| 93 |
-
time.sleep(0.
|
| 94 |
-
response = requests.get(current_url, headers=headers, timeout=
|
| 95 |
|
| 96 |
content_type = response.headers.get('Content-Type', '').lower()
|
| 97 |
if 'text/html' not in content_type:
|
|
@@ -112,7 +106,7 @@ def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[Li
|
|
| 112 |
# Logic: Internal Domain, No Files, No System Pages
|
| 113 |
is_internal = parsed_url.netloc == domain or parsed_url.netloc == ''
|
| 114 |
is_file = any(clean_url.endswith(ext) for ext in ['.pdf', '.jpg', '.png', '.zip', '.css', '.js'])
|
| 115 |
-
is_ignored = any(x in clean_url.lower() for x in ['/login', '/cart', '/checkout', '
|
| 116 |
|
| 117 |
if is_internal and not is_file and not is_ignored:
|
| 118 |
if clean_url not in visited:
|
|
@@ -185,7 +179,7 @@ def fetch_sitemap_links(user_input: str) -> Tuple[List[str], str]:
|
|
| 185 |
return crawl_and_generate_sitemap(base_url)
|
| 186 |
|
| 187 |
# ============================================================================
|
| 188 |
-
# 3.
|
| 189 |
# ============================================================================
|
| 190 |
|
| 191 |
def classify_stage(url: str) -> str:
|
|
@@ -201,14 +195,11 @@ def expand_topic_variants(topic_slug: str) -> Set[str]:
|
|
| 201 |
variants.add(topic_slug.replace('-', ''))
|
| 202 |
variants.add(topic_slug.replace('-', ' '))
|
| 203 |
|
| 204 |
-
# Add pack keywords
|
| 205 |
for pack_name, keywords in TOPIC_PACKS.items():
|
| 206 |
if topic_slug in [normalize_str(k) for k in keywords] or topic_slug == pack_name:
|
| 207 |
for kw in keywords:
|
| 208 |
n_kw = normalize_str(kw)
|
| 209 |
-
variants.
|
| 210 |
-
variants.add(n_kw.replace('-', ''))
|
| 211 |
-
variants.add(n_kw.replace('-', ' '))
|
| 212 |
break
|
| 213 |
return variants
|
| 214 |
|
|
@@ -217,6 +208,7 @@ def categorize_links(urls: List[str]) -> Dict[str, List[str]]:
|
|
| 217 |
for url in urls:
|
| 218 |
norm = path_norm(url)
|
| 219 |
assigned = False
|
|
|
|
| 220 |
# 1. Check Hints
|
| 221 |
for cat_name, hints in CATEGORY_HINTS.items():
|
| 222 |
if any(h in norm for h in hints):
|
|
@@ -225,16 +217,25 @@ def categorize_links(urls: List[str]) -> Dict[str, List[str]]:
|
|
| 225 |
categories[cat_name].append(topic)
|
| 226 |
assigned = True
|
| 227 |
break
|
| 228 |
-
|
|
|
|
| 229 |
if not assigned:
|
| 230 |
parts = list(filter(None, urlparse(url).path.split('/')))
|
| 231 |
if len(parts) > 0:
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
return dict(categories)
|
| 234 |
|
| 235 |
def match_topic_urls(links, topic, category, match_mode, synonyms="", neg_keywords=""):
|
| 236 |
# Filter Category
|
| 237 |
if category and category != "All":
|
|
|
|
| 238 |
filtered = [u for u in links if category in categorize_links([u])]
|
| 239 |
else:
|
| 240 |
filtered = links
|
|
@@ -271,11 +272,9 @@ def match_topic_urls(links, topic, category, match_mode, synonyms="", neg_keywor
|
|
| 271 |
return [x[0] for x in results]
|
| 272 |
|
| 273 |
def score_ai_output(output_text: str, topic: str, stage: str) -> dict:
|
| 274 |
-
"""7-Point Scoring System"""
|
| 275 |
scores = {'total': 0}
|
| 276 |
lower_out = output_text.lower()
|
| 277 |
|
| 278 |
-
# Scoring Logic
|
| 279 |
matches = sum(lower_out.count(w) for w in normalize_str(topic).split('-'))
|
| 280 |
scores['Relevance'] = min(20, matches * 2)
|
| 281 |
|
|
@@ -303,14 +302,12 @@ def generate_radar(user_urls, competitors):
|
|
| 303 |
categories = ['TOFU', 'MOFU', 'BOFU']
|
| 304 |
fig = go.Figure()
|
| 305 |
|
| 306 |
-
# User Data
|
| 307 |
if user_urls:
|
| 308 |
stages = [classify_stage(u) for u in user_urls]
|
| 309 |
total = len(stages) or 1
|
| 310 |
stats = [(stages.count(s)/total)*100 for s in categories]
|
| 311 |
fig.add_trace(go.Scatterpolar(r=stats, theta=categories, fill='toself', name='You', line_color='#3b82f6'))
|
| 312 |
|
| 313 |
-
# Competitor Data
|
| 314 |
colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6']
|
| 315 |
for i, c in enumerate(competitors):
|
| 316 |
s = c['stats']
|
|
@@ -346,14 +343,12 @@ def analyze_gaps(user_urls, competitors, filter_topic):
|
|
| 346 |
# 5. UI EVENT HANDLERS
|
| 347 |
# ============================================================================
|
| 348 |
|
| 349 |
-
state = {'urls': [], 'categories': {}, 'competitors': []}
|
| 350 |
-
|
| 351 |
def ui_fetch_data(url):
|
| 352 |
urls, msg = fetch_sitemap_links(url)
|
| 353 |
cats = categorize_links(urls)
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
return urls, cats, msg,
|
| 357 |
|
| 358 |
def ui_analyze_planner(urls, category, topic, mode, syns, negs):
|
| 359 |
if not urls: return "Fetch data first.", None, None
|
|
@@ -375,28 +370,20 @@ def ui_analyze_planner(urls, category, topic, mode, syns, negs):
|
|
| 375 |
return summary, matched, None
|
| 376 |
|
| 377 |
def ui_generate_ideas(topic, stage):
|
| 378 |
-
# Mock AI Generation (Replace with actual API key in production)
|
| 379 |
mock_res = f"""# ๐ {stage} Content Ideas for {topic}
|
| 380 |
-
|
| 381 |
## 1. The Ultimate Guide to {topic}
|
| 382 |
**Format:** Blog Post
|
| 383 |
**Why:** Captures high-volume search traffic.
|
| 384 |
-
**Structure:** Definition, Benefits, Trends.
|
| 385 |
-
|
| 386 |
## 2. {topic} Maturity Model
|
| 387 |
**Format:** Whitepaper
|
| 388 |
**Why:** Helps leads evaluate their current state.
|
| 389 |
-
|
| 390 |
-
## 3. Case Study: Solving {topic} at Scale
|
| 391 |
-
**Format:** Success Story
|
| 392 |
-
**Why:** Social proof for decision makers.
|
| 393 |
"""
|
| 394 |
scores = score_ai_output(mock_res, topic, stage)
|
| 395 |
score_txt = f"### ๐ AI Score: {scores['total']}/100\n" + "\n".join(f"- {k}: {v}" for k,v in scores.items() if k!='total')
|
| 396 |
return mock_res, score_txt
|
| 397 |
|
| 398 |
def ui_add_competitor(url, current_comps):
|
| 399 |
-
urls, msg = fetch_sitemap_links(url)
|
| 400 |
domain = urlparse(url if url.startswith('http') else f'https://{url}').netloc
|
| 401 |
|
| 402 |
stages = [classify_stage(u) for u in urls]
|
|
@@ -441,7 +428,8 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
|
|
| 441 |
fetch_log = gr.Textbox(label="Status", lines=1)
|
| 442 |
|
| 443 |
with gr.Row():
|
| 444 |
-
|
|
|
|
| 445 |
topic_in = gr.Textbox(label="Topic")
|
| 446 |
|
| 447 |
with gr.Accordion("Advanced Matching", open=False):
|
|
@@ -485,13 +473,4 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
|
|
| 485 |
|
| 486 |
analyze_btn.click(ui_analyze_planner,
|
| 487 |
[state_urls_ui, cat_dd, topic_in, match_mode, synonyms, neg_kw],
|
| 488 |
-
[plan_summary, plan_urls, gr.State()])
|
| 489 |
-
|
| 490 |
-
gen_btn.click(ui_generate_ideas, [topic_in, gen_stage], [idea_out, score_out])
|
| 491 |
-
|
| 492 |
-
add_comp.click(ui_add_competitor, [comp_url, state_comps_ui], [state_comps_ui, comp_display, comp_url])
|
| 493 |
-
|
| 494 |
-
comp_analyze.click(ui_analyze_comp_full, [state_urls_ui, state_comps_ui, gap_filter], [radar_plot, gap_res])
|
| 495 |
-
|
| 496 |
-
if __name__ == "__main__":
|
| 497 |
-
demo.launch()
|
|
|
|
| 1 |
"""
|
| 2 |
+
B2B Content Strategy Planner (Final Fixed Edition)
|
| 3 |
+
Deployed for Hugging Face Spaces.
|
| 4 |
"""
|
| 5 |
|
| 6 |
import gradio as gr
|
|
|
|
| 16 |
from typing import List, Dict, Set, Tuple, Optional
|
| 17 |
|
| 18 |
# ============================================================================
|
| 19 |
+
# 1. CONFIGURATION & TOPIC PACKS
|
| 20 |
# ============================================================================
|
| 21 |
|
| 22 |
TOPIC_PACKS = {
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
# ============================================================================
|
| 50 |
+
# 2. CRAWLER LOGIC (Smart & Deep)
|
| 51 |
# ============================================================================
|
| 52 |
|
| 53 |
def normalize_str(s: str) -> str:
|
|
|
|
| 63 |
except:
|
| 64 |
return normalize_str(url)
|
| 65 |
|
|
|
|
|
|
|
|
|
|
| 66 |
def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[List[str], str]:
|
| 67 |
+
"""Crawls a website to generate a sitemap dynamically."""
|
|
|
|
|
|
|
|
|
|
| 68 |
if not start_url.startswith(('http://', 'https://')):
|
| 69 |
start_url = f'https://{start_url}'
|
| 70 |
|
|
|
|
| 84 |
while queue and len(found_urls) < max_pages:
|
| 85 |
current_url = queue.pop(0)
|
| 86 |
try:
|
| 87 |
+
time.sleep(0.05) # Polite delay
|
| 88 |
+
response = requests.get(current_url, headers=headers, timeout=6)
|
| 89 |
|
| 90 |
content_type = response.headers.get('Content-Type', '').lower()
|
| 91 |
if 'text/html' not in content_type:
|
|
|
|
| 106 |
# Logic: Internal Domain, No Files, No System Pages
|
| 107 |
is_internal = parsed_url.netloc == domain or parsed_url.netloc == ''
|
| 108 |
is_file = any(clean_url.endswith(ext) for ext in ['.pdf', '.jpg', '.png', '.zip', '.css', '.js'])
|
| 109 |
+
is_ignored = any(x in clean_url.lower() for x in ['/login', '/cart', '/checkout', 'javascript:', 'mailto:', 'tel:'])
|
| 110 |
|
| 111 |
if is_internal and not is_file and not is_ignored:
|
| 112 |
if clean_url not in visited:
|
|
|
|
| 179 |
return crawl_and_generate_sitemap(base_url)
|
| 180 |
|
| 181 |
# ============================================================================
|
| 182 |
+
# 3. ANALYSIS & CATEGORIZATION
|
| 183 |
# ============================================================================
|
| 184 |
|
| 185 |
def classify_stage(url: str) -> str:
|
|
|
|
| 195 |
variants.add(topic_slug.replace('-', ''))
|
| 196 |
variants.add(topic_slug.replace('-', ' '))
|
| 197 |
|
|
|
|
| 198 |
for pack_name, keywords in TOPIC_PACKS.items():
|
| 199 |
if topic_slug in [normalize_str(k) for k in keywords] or topic_slug == pack_name:
|
| 200 |
for kw in keywords:
|
| 201 |
n_kw = normalize_str(kw)
|
| 202 |
+
variants.update([n_kw, n_kw.replace('-', ''), n_kw.replace('-', ' ')])
|
|
|
|
|
|
|
| 203 |
break
|
| 204 |
return variants
|
| 205 |
|
|
|
|
| 208 |
for url in urls:
|
| 209 |
norm = path_norm(url)
|
| 210 |
assigned = False
|
| 211 |
+
|
| 212 |
# 1. Check Hints
|
| 213 |
for cat_name, hints in CATEGORY_HINTS.items():
|
| 214 |
if any(h in norm for h in hints):
|
|
|
|
| 217 |
categories[cat_name].append(topic)
|
| 218 |
assigned = True
|
| 219 |
break
|
| 220 |
+
|
| 221 |
+
# 2. Check Folder Structure (Fallback)
|
| 222 |
if not assigned:
|
| 223 |
parts = list(filter(None, urlparse(url).path.split('/')))
|
| 224 |
if len(parts) > 0:
|
| 225 |
+
cat = parts[0].title()
|
| 226 |
+
# Avoid junk categories (dates, short codes)
|
| 227 |
+
if len(cat) > 2 and not cat.isdigit():
|
| 228 |
+
categories[cat].append(parts[-1])
|
| 229 |
+
else:
|
| 230 |
+
# 3. Root Level / Uncategorized
|
| 231 |
+
categories["General"].append(norm)
|
| 232 |
+
|
| 233 |
return dict(categories)
|
| 234 |
|
| 235 |
def match_topic_urls(links, topic, category, match_mode, synonyms="", neg_keywords=""):
|
| 236 |
# Filter Category
|
| 237 |
if category and category != "All":
|
| 238 |
+
# Only include if URL belongs to selected category
|
| 239 |
filtered = [u for u in links if category in categorize_links([u])]
|
| 240 |
else:
|
| 241 |
filtered = links
|
|
|
|
| 272 |
return [x[0] for x in results]
|
| 273 |
|
| 274 |
def score_ai_output(output_text: str, topic: str, stage: str) -> dict:
|
|
|
|
| 275 |
scores = {'total': 0}
|
| 276 |
lower_out = output_text.lower()
|
| 277 |
|
|
|
|
| 278 |
matches = sum(lower_out.count(w) for w in normalize_str(topic).split('-'))
|
| 279 |
scores['Relevance'] = min(20, matches * 2)
|
| 280 |
|
|
|
|
| 302 |
categories = ['TOFU', 'MOFU', 'BOFU']
|
| 303 |
fig = go.Figure()
|
| 304 |
|
|
|
|
| 305 |
if user_urls:
|
| 306 |
stages = [classify_stage(u) for u in user_urls]
|
| 307 |
total = len(stages) or 1
|
| 308 |
stats = [(stages.count(s)/total)*100 for s in categories]
|
| 309 |
fig.add_trace(go.Scatterpolar(r=stats, theta=categories, fill='toself', name='You', line_color='#3b82f6'))
|
| 310 |
|
|
|
|
| 311 |
colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6']
|
| 312 |
for i, c in enumerate(competitors):
|
| 313 |
s = c['stats']
|
|
|
|
| 343 |
# 5. UI EVENT HANDLERS
|
| 344 |
# ============================================================================
|
| 345 |
|
|
|
|
|
|
|
| 346 |
def ui_fetch_data(url):
|
| 347 |
urls, msg = fetch_sitemap_links(url)
|
| 348 |
cats = categorize_links(urls)
|
| 349 |
+
# KEY FIX: Return gr.update for the Dropdown
|
| 350 |
+
cat_choices = ["All"] + list(cats.keys())
|
| 351 |
+
return urls, cats, msg, gr.update(choices=cat_choices, value="All")
|
| 352 |
|
| 353 |
def ui_analyze_planner(urls, category, topic, mode, syns, negs):
|
| 354 |
if not urls: return "Fetch data first.", None, None
|
|
|
|
| 370 |
return summary, matched, None
|
| 371 |
|
| 372 |
def ui_generate_ideas(topic, stage):
|
|
|
|
| 373 |
mock_res = f"""# ๐ {stage} Content Ideas for {topic}
|
|
|
|
| 374 |
## 1. The Ultimate Guide to {topic}
|
| 375 |
**Format:** Blog Post
|
| 376 |
**Why:** Captures high-volume search traffic.
|
|
|
|
|
|
|
| 377 |
## 2. {topic} Maturity Model
|
| 378 |
**Format:** Whitepaper
|
| 379 |
**Why:** Helps leads evaluate their current state.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
"""
|
| 381 |
scores = score_ai_output(mock_res, topic, stage)
|
| 382 |
score_txt = f"### ๐ AI Score: {scores['total']}/100\n" + "\n".join(f"- {k}: {v}" for k,v in scores.items() if k!='total')
|
| 383 |
return mock_res, score_txt
|
| 384 |
|
| 385 |
def ui_add_competitor(url, current_comps):
|
| 386 |
+
urls, msg = fetch_sitemap_links(url)
|
| 387 |
domain = urlparse(url if url.startswith('http') else f'https://{url}').netloc
|
| 388 |
|
| 389 |
stages = [classify_stage(u) for u in urls]
|
|
|
|
| 428 |
fetch_log = gr.Textbox(label="Status", lines=1)
|
| 429 |
|
| 430 |
with gr.Row():
|
| 431 |
+
# FIXED: Dropdown is now updated properly via gr.update
|
| 432 |
+
cat_dd = gr.Dropdown(label="Category", choices=[], allow_custom_value=True)
|
| 433 |
topic_in = gr.Textbox(label="Topic")
|
| 434 |
|
| 435 |
with gr.Accordion("Advanced Matching", open=False):
|
|
|
|
| 473 |
|
| 474 |
analyze_btn.click(ui_analyze_planner,
|
| 475 |
[state_urls_ui, cat_dd, topic_in, match_mode, synonyms, neg_kw],
|
| 476 |
+
[plan_summary, plan_urls, gr.State()])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|