mktgtech commited on
Commit
fe2e2f5
ยท
verified ยท
1 Parent(s): 75a8fd2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +29 -50
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- B2B Content Strategy Planner (Hybrid Edition)
3
- Combines your Working Smart Crawler with Advanced Analytics (Radar Charts, AI Scoring, Gap Analysis).
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 (FULL)
20
  # ============================================================================
21
 
22
  TOPIC_PACKS = {
@@ -47,7 +47,7 @@ CATEGORY_HINTS = {
47
  }
48
 
49
  # ============================================================================
50
- # 2. YOUR WORKING CRAWLER LOGIC (PRESERVED)
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.1) # Respectful delay
94
- response = requests.get(current_url, headers=headers, timeout=8)
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', '/account', 'javascript:', 'mailto:'])
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. ADVANCED ANALYSIS (Merged from my code)
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.add(n_kw)
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
- # 2. Check Folder Structure
 
229
  if not assigned:
230
  parts = list(filter(None, urlparse(url).path.split('/')))
231
  if len(parts) > 0:
232
- categories[parts[0].title()].append(parts[-1])
 
 
 
 
 
 
 
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
- state['urls'] = urls
355
- state['categories'] = cats
356
- return urls, cats, msg, list(cats.keys())
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) # Uses same hybrid crawler
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
- cat_dd = gr.Dropdown(label="Category", choices=[])
 
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()])