mktgtech commited on
Commit
6ca3f5a
·
verified ·
1 Parent(s): 70b418e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -157
app.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
- B2B Content Strategy Planner (Final Corrected Edition)
3
  Deployed for Hugging Face Spaces.
 
4
  """
5
 
6
  import gradio as gr
@@ -47,7 +48,7 @@ CATEGORY_HINTS = {
47
  }
48
 
49
  # ============================================================================
50
- # 2. CRAWLER LOGIC
51
  # ============================================================================
52
 
53
  def normalize_str(s: str) -> str:
@@ -61,7 +62,7 @@ def path_norm(url: str) -> str:
61
  except:
62
  return normalize_str(url)
63
 
64
- def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[List[str], str]:
65
  if not start_url.startswith(('http://', 'https://')):
66
  start_url = f'https://{start_url}'
67
 
@@ -79,7 +80,7 @@ def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[Li
79
  current_url = queue.pop(0)
80
  try:
81
  time.sleep(0.05)
82
- response = requests.get(current_url, headers=headers, timeout=6)
83
  if 'text/html' not in response.headers.get('Content-Type', '').lower(): continue
84
 
85
  soup = BeautifulSoup(response.text, 'html.parser')
@@ -104,14 +105,6 @@ def crawl_and_generate_sitemap(start_url: str, max_pages: int = 150) -> Tuple[Li
104
  if not found_urls: return [], f"❌ Could not crawl {start_url}."
105
  return list(set(found_urls)), f"{status_log}✅ Successfully crawled {len(found_urls)} pages."
106
 
107
- def parse_xml_text(xml_text: str) -> List[str]:
108
- urls = []
109
- loc_regex = re.compile(r'<loc>(.*?)</loc>')
110
- for match in loc_regex.finditer(xml_text):
111
- url = match.group(1).strip()
112
- if url.startswith(('http://', 'https://')): urls.append(url)
113
- return urls
114
-
115
  def fetch_sitemap_recursive(url: str, visited: Set[str] = None, depth: int = 0) -> List[str]:
116
  if visited is None: visited = set()
117
  if depth > 3 or url in visited: return []
@@ -122,14 +115,18 @@ def fetch_sitemap_recursive(url: str, visited: Set[str] = None, depth: int = 0)
122
  xml_text = response.text
123
  except: return []
124
 
125
- direct_urls = parse_xml_text(xml_text)
 
 
 
 
126
  if '<sitemapindex' in xml_text.lower():
127
- child_sitemaps = direct_urls[:15]
128
  all_urls = []
129
  for child_url in child_sitemaps:
130
  all_urls.extend(fetch_sitemap_recursive(child_url, visited, depth + 1))
131
  return list(set(all_urls))
132
- return direct_urls
133
 
134
  def fetch_sitemap_links(user_input: str) -> Tuple[List[str], str]:
135
  user_input = user_input.strip()
@@ -221,124 +218,149 @@ def match_topic_urls(links, topic, category, match_mode, synonyms="", neg_keywor
221
  return [x[0] for x in results]
222
 
223
  # ============================================================================
224
- # 4. INTELLIGENCE: GLOBAL GAPS & STRENGTHS
225
  # ============================================================================
226
 
227
- def get_tokens(urls: List[str]) -> Counter:
228
- tokens = []
229
- for u in urls:
230
- slug = path_norm(u)
231
- words = re.split(r'[-/]', slug)
232
- tokens.extend([w for w in words if len(w) > 3 and not w.isdigit()])
233
- return Counter(tokens)
234
-
235
- def analyze_global_positioning(user_urls, competitors):
236
- if not user_urls or not competitors:
237
- return "⚠️ Data missing for analysis.", "⚠️ Data missing for analysis."
238
-
239
- user_tokens = get_tokens(user_urls)
240
- comp_tokens = Counter()
241
- for c in competitors:
242
- comp_tokens.update(get_tokens(c['urls']))
243
-
244
- gaps = []
245
- for token, count in comp_tokens.most_common(50):
246
- if token not in user_tokens or user_tokens[token] < 2:
247
- gaps.append(f"**{token}** (Competitors: {count}, You: {user_tokens[token]})")
248
-
249
- strengths = []
250
- for token, count in user_tokens.most_common(50):
251
- if token not in comp_tokens or comp_tokens[token] < 2:
252
- strengths.append(f"**{token}** (You: {count}, Competitors: {comp_tokens[token]})")
253
-
254
- gap_txt = "### 🚨 Global Content Gaps (High Opportunity)\n" + "\n".join([f"- {g}" for g in gaps[:15]])
255
- str_txt = "### 💪 Your Unique Strengths\n" + "\n".join([f"- {s}" for s in strengths[:15]])
256
 
257
- return gap_txt, str_txt
 
 
 
 
 
 
 
 
 
258
 
259
- def analyze_custom_keyword(user_urls, competitors, keyword):
260
- if not keyword: return "⚠️ Please enter a keyword."
261
- kw = normalize_str(keyword)
262
 
263
- user_matches = [u for u in user_urls if kw in path_norm(u)]
264
- user_count = len(user_matches)
265
 
266
- comp_stats = []
267
- total_comp_count = 0
268
- for c in competitors:
269
- c_matches = [u for u in c['urls'] if kw in path_norm(u)]
270
- count = len(c_matches)
271
- total_comp_count += count
272
- comp_stats.append(f"- **{c['domain']}**: {count} pages")
 
 
 
 
 
 
 
273
 
274
- avg_comp = total_comp_count / len(competitors) if competitors else 0
275
 
276
- if user_count == 0 and total_comp_count > 0:
277
- verdict = "🔴 **CRITICAL GAP:** Competitors are covering this, you are not."
278
- elif user_count > total_comp_count:
279
- verdict = "🟢 **DOMINANT STRENGTH:** You own this topic compared to competitors."
280
- elif user_count > 0 and user_count < avg_comp:
281
- verdict = "🟡 **COMPETITIVE DISADVANTAGE:** You have content, but competitors have more depth."
282
- else:
283
- verdict = "⚪ **NEUTRAL:** Comparable coverage."
284
-
285
- report = f"""### 🧐 Keyword Analysis: "{keyword}"
286
- {verdict}
287
-
288
- **Your Coverage:** {user_count} pages
289
- **Competitor Coverage:**
290
- {chr(10).join(comp_stats)}
291
- """
292
- return report
293
 
294
  # ============================================================================
295
- # 5. UI GENERATION
296
  # ============================================================================
297
 
298
- def ui_generate_ideas_multimodal(topic, stage):
299
- ideas = [
300
- f"1. **[Blog]** The Ultimate Guide to {topic} for {time.strftime('%Y')}",
301
- f"2. **[Video 🎥]** {topic} Explained in 5 Minutes (Script Outline)",
302
- f"3. **[Whitepaper 📄]** The State of {topic}: Industry Report",
303
- f"4. **[Infographic 🖼️]** 7 Key Stats About {topic} You Can't Ignore",
304
- f"5. **[Webinar 🎤]** Live Q&A: Solving Common {topic} Challenges",
305
- f"6. **[Case Study]** How [Industry Leader] Scaled {topic} Successfully",
306
- f"7. **[Podcast 🎙️]** Interview with a {topic} Expert",
307
- f"8. **[Checklist ✅]** The 10-Point {topic} Implementation Audit",
308
- f"9. **[Social Thread 🧵]** 5 Myths About {topic} Debunked",
309
- f"10. **[Tool/Calculator 🧮]** Calculate Your {topic} ROI"
310
- ]
311
- formatted_ideas = f"# 🚀 10 Multimodal Content Ideas for '{topic}' ({stage})\n\n" + "\n\n".join(ideas)
312
- score_txt = f"""### 🏆 AI Strategy Score: 92/100
313
- - **Relevance:** 20/20
314
- - **Format Diversity:** 20/20
315
- - **Stage Alignment:** 18/20
316
- - **Actionability:** 18/20
317
  """
318
- return formatted_ideas, score_txt
319
-
320
- def ui_analyze_planner(urls, category, topic, mode, syns, negs):
321
- if not urls: return "Fetch data first.", None, None
322
- if not topic: return "Please select or enter a topic.", None, None
323
 
324
- matched = match_topic_urls(urls, topic, category, mode, syns, negs)
325
 
326
- stages = [classify_stage(u) for u in matched]
327
- total = len(stages) or 1
328
- tofu = (stages.count('TOFU')/total)*100
329
- mofu = (stages.count('MOFU')/total)*100
330
- bofu = (stages.count('BOFU')/total)*100
331
 
332
- summary = f"### Analysis: {topic}\n**Matched Pages:** {len(matched)}\n\n"
333
- summary += f"**Funnel Health:**\n- TOFU: {tofu:.1f}%\n- MOFU: {mofu:.1f}%\n- BOFU: {bofu:.1f}%\n\n"
 
334
 
335
- if tofu < 20: summary += "⚠️ **Recommendation:** Create more Awareness content (Blogs, Glossaries).\n"
336
- if mofu < 20: summary += "⚠️ **Recommendation:** Create more Consideration content (Whitepapers, Comparisons).\n"
 
 
 
 
 
 
 
 
337
 
338
- return summary, matched, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  # ============================================================================
341
- # 6. GRADIO INTERFACE
342
  # ============================================================================
343
 
344
  state = {'urls': [], 'categories': {}, 'competitors': []}
@@ -347,7 +369,7 @@ def ui_fetch(url):
347
  urls, msg = fetch_sitemap_links(url)
348
  cats = categorize_links(urls)
349
  cat_list = ["All"] + sorted(list(cats.keys()))
350
- return urls, cats, msg, gr.update(choices=cat_list, value="All"), gr.update(choices=[], value=None)
351
 
352
  def ui_update_topics(category, categories_data):
353
  if not categories_data: return gr.update(choices=[])
@@ -356,6 +378,21 @@ def ui_update_topics(category, categories_data):
356
  return gr.update(choices=all_topics)
357
  return gr.update(choices=sorted(categories_data.get(category, [])))
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  def ui_add_comp(url, current_comps):
360
  urls, msg = fetch_sitemap_links(url)
361
  domain = urlparse(url if url.startswith('http') else f'https://{url}').netloc
@@ -368,27 +405,17 @@ def ui_add_comp(url, current_comps):
368
  txt = "\n".join([f"✅ {c['domain']}: {c['stats']['Count']} pages" for c in current_comps])
369
  return current_comps, txt, ""
370
 
371
- def ui_analyze_comp_master(user_urls, competitors, custom_kw):
372
- if not user_urls: return None, "Fetch Data First", "Fetch Data First", "Fetch Data First"
373
- radar = generate_radar(user_urls, competitors)
374
- gaps, strengths = analyze_global_positioning(user_urls, competitors)
375
- kw_report = analyze_custom_keyword(user_urls, competitors, custom_kw) if custom_kw else "Enter a keyword above to analyze specific topic coverage."
376
- return radar, gaps, strengths, kw_report
377
-
378
- def generate_radar(user_urls, competitors):
379
- fig = go.Figure()
380
- cats = ['TOFU', 'MOFU', 'BOFU']
381
- if user_urls:
382
- stages = [classify_stage(u) for u in user_urls]
383
- tot = len(stages) or 1
384
- fig.add_trace(go.Scatterpolar(r=[(stages.count(s)/tot)*100 for s in cats], theta=cats, fill='toself', name='You'))
385
 
386
- for c in competitors:
387
- s = c['stats']
388
- fig.add_trace(go.Scatterpolar(r=[s['TOFU'], s['MOFU'], s['BOFU']], theta=cats, fill='toself', name=c['domain']))
389
 
390
- fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), title="Strategy Radar")
391
- return fig
392
 
393
  # ============================================================================
394
  # LAYOUT
@@ -399,7 +426,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
399
  state_cats_ui = gr.State({})
400
  state_comps_ui = gr.State([])
401
 
402
- gr.Markdown("# 🚀 B2B Content Strategy Master")
403
 
404
  with gr.Tabs():
405
  # TAB 1: PLANNER
@@ -413,25 +440,22 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
413
  cat_dd = gr.Dropdown(label="Category", choices=[], allow_custom_value=True)
414
  topic_dd = gr.Dropdown(label="Topic", choices=[], allow_custom_value=True)
415
 
416
- # --- RESTORED: Advanced Filters & Analyze Button ---
417
- with gr.Accordion("Advanced Matching", open=False):
418
  match_mode = gr.Radio(["fuzzy", "exact"], label="Mode", value="fuzzy")
419
- synonyms = gr.Textbox(label="Synonyms (comma-separated)")
420
  neg_kw = gr.Textbox(label="Negative Keywords")
421
 
422
- analyze_btn = gr.Button("Analyze Content Coverage", variant="secondary")
423
 
424
  with gr.Row():
425
- plan_summary = gr.Markdown(label="Analysis Results")
426
  plan_urls = gr.Textbox(label="Matched URLs", lines=5)
427
- # ---------------------------------------------------
428
 
429
- gr.Markdown("### ✨ AI Idea Generator")
430
- gen_btn = gr.Button("✨ Generate 10 Multimodal Ideas", variant="primary")
 
431
 
432
- with gr.Row():
433
- idea_out = gr.Markdown()
434
- score_out = gr.Markdown()
435
 
436
  # TAB 2: COMPETITORS
437
  with gr.Tab("🏆 Competitor Intelligence"):
@@ -440,33 +464,36 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
440
  add_comp = gr.Button("Add Competitor")
441
  comp_display = gr.Markdown("No competitors added.")
442
 
443
- gr.Markdown("### 🕵️ Custom Deep Dive")
 
 
444
  with gr.Row():
445
- custom_kw_in = gr.Textbox(label="Analyze Specific Keyword (e.g., 'Generative AI')")
446
- comp_analyze = gr.Button("Analyze All (Gaps, Strengths & Keyword)", variant="primary")
 
447
 
448
  with gr.Row():
449
  radar_plot = gr.Plot(label="Strategy Radar")
450
- kw_report_out = gr.Markdown(label="Keyword Analysis")
451
 
452
  with gr.Row():
453
- gap_res = gr.Markdown()
454
- str_res = gr.Markdown()
 
455
 
456
  # EVENTS
457
  fetch_btn.click(ui_fetch, [url_input], [state_urls_ui, state_cats_ui, fetch_log, cat_dd, topic_dd])
458
  cat_dd.change(ui_update_topics, [cat_dd, state_cats_ui], [topic_dd])
459
-
460
- # Wired the missing Analyze Button
461
  analyze_btn.click(ui_analyze_planner, [state_urls_ui, cat_dd, topic_dd, match_mode, synonyms, neg_kw], [plan_summary, plan_urls, gr.State()])
462
 
463
- gen_btn.click(ui_generate_ideas_multimodal, [topic_dd, gr.State("TOFU")], [idea_out, score_out])
 
464
 
465
  add_comp.click(ui_add_comp, [comp_url, state_comps_ui], [state_comps_ui, comp_display, comp_url])
466
 
467
- comp_analyze.click(ui_analyze_comp_master,
468
- [state_urls_ui, state_comps_ui, custom_kw_in],
469
- [radar_plot, gap_res, str_res, kw_report_out])
 
470
 
471
  if __name__ == "__main__":
472
  demo.launch()
 
1
  """
2
+ B2B Content Strategy Planner (Multi-Model & Competitive Intelligence Edition)
3
  Deployed for Hugging Face Spaces.
4
+ Features: Multi-AI Model Simulation, Structured SEO Ideas, Targeted Gap/Strength Analysis.
5
  """
6
 
7
  import gradio as gr
 
48
  }
49
 
50
  # ============================================================================
51
+ # 2. CRAWLER LOGIC (Preserved & Optimized)
52
  # ============================================================================
53
 
54
  def normalize_str(s: str) -> str:
 
62
  except:
63
  return normalize_str(url)
64
 
65
+ def crawl_and_generate_sitemap(start_url: str, max_pages: int = 200) -> Tuple[List[str], str]:
66
  if not start_url.startswith(('http://', 'https://')):
67
  start_url = f'https://{start_url}'
68
 
 
80
  current_url = queue.pop(0)
81
  try:
82
  time.sleep(0.05)
83
+ response = requests.get(current_url, headers=headers, timeout=5)
84
  if 'text/html' not in response.headers.get('Content-Type', '').lower(): continue
85
 
86
  soup = BeautifulSoup(response.text, 'html.parser')
 
105
  if not found_urls: return [], f"❌ Could not crawl {start_url}."
106
  return list(set(found_urls)), f"{status_log}✅ Successfully crawled {len(found_urls)} pages."
107
 
 
 
 
 
 
 
 
 
108
  def fetch_sitemap_recursive(url: str, visited: Set[str] = None, depth: int = 0) -> List[str]:
109
  if visited is None: visited = set()
110
  if depth > 3 or url in visited: return []
 
115
  xml_text = response.text
116
  except: return []
117
 
118
+ urls = []
119
+ for match in re.finditer(r'<loc>(.*?)</loc>', xml_text):
120
+ u = match.group(1).strip()
121
+ if u.startswith(('http://', 'https://')): urls.append(u)
122
+
123
  if '<sitemapindex' in xml_text.lower():
124
+ child_sitemaps = urls[:15]
125
  all_urls = []
126
  for child_url in child_sitemaps:
127
  all_urls.extend(fetch_sitemap_recursive(child_url, visited, depth + 1))
128
  return list(set(all_urls))
129
+ return urls
130
 
131
  def fetch_sitemap_links(user_input: str) -> Tuple[List[str], str]:
132
  user_input = user_input.strip()
 
218
  return [x[0] for x in results]
219
 
220
  # ============================================================================
221
+ # 4. MULTI-MODEL AI GENERATION
222
  # ============================================================================
223
 
224
+ def generate_idea_card(model_name, topic, index):
225
+ """Generates a structured SEO idea card"""
226
+ # Simulate diversity in content types based on index
227
+ types = ["Blog Post", "Video Script", "Whitepaper", "Case Study", "Infographic", "Podcast", "Webinar", "Checklist", "Social Thread", "Interactive Tool"]
228
+ content_type = types[index % len(types)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
+ return f"""
231
+ ### 💡 Idea {index + 1}: {topic.title()} Mastery ({model_name})
232
+
233
+ **📝 Title:** The Definitive Guide to {topic.title()} in {time.strftime('%Y')}
234
+ **📄 Description:** A comprehensive deep-dive into {topic}, covering emerging trends and actionable strategies for B2B leaders.
235
+ **🎥 Content Type:** {content_type}
236
+ **🔑 Key Topics:** {topic} implementation, best practices, common pitfalls, ROI calculation.
237
+ **🚀 Differentiation:** Focuses on proprietary data and real-world failure stories, unlike generic competitor posts.
238
+ ---
239
+ """
240
 
241
+ def ui_generate_multi_model_ideas(topic, stage):
242
+ if not topic: return "⚠️ Please select a topic first.", ""
 
243
 
244
+ # Simulate Multi-Model Processing
245
+ outputs = []
246
 
247
+ # Model 1: The Strategist (GPT-4 Style)
248
+ outputs.append("## 🧠 Model A: The Strategist (Analytical Focus)\n")
249
+ for i in range(3):
250
+ outputs.append(generate_idea_card("Strategist", topic, i))
251
+
252
+ # Model 2: The Creative (Claude Style)
253
+ outputs.append("\n## 🎨 Model B: The Creative (Engagement Focus)\n")
254
+ for i in range(3, 6):
255
+ outputs.append(generate_idea_card("Creative", topic, i))
256
+
257
+ # Model 3: The SEO Expert (Llama Style)
258
+ outputs.append("\n## 📈 Model C: The SEO Expert (Traffic Focus)\n")
259
+ for i in range(6, 10):
260
+ outputs.append(generate_idea_card("SEO Expert", topic, i))
261
 
262
+ final_output = "\n".join(outputs)
263
 
264
+ return final_output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
  # ============================================================================
267
+ # 5. TARGETED COMPETITOR INTELLIGENCE
268
  # ============================================================================
269
 
270
+ def analyze_topic_gap_strength(user_urls, competitors, topic):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  """
272
+ Analyzes a specific topic to determine if it's a GAP or STRENGTH.
273
+ """
274
+ if not topic: return "⚠️ Enter a topic to analyze.", "⚠️ Enter a topic to analyze."
 
 
275
 
276
+ kw = normalize_str(topic)
277
 
278
+ # User Coverage
279
+ user_matches = [u for u in user_urls if kw in path_norm(u)]
280
+ user_count = len(user_matches)
 
 
281
 
282
+ # Competitor Coverage
283
+ comp_matches_total = 0
284
+ comp_details = []
285
 
286
+ for c in competitors:
287
+ matches = [u for u in c['urls'] if kw in path_norm(u)]
288
+ count = len(matches)
289
+ comp_matches_total += count
290
+ if count > 0:
291
+ comp_details.append(f"- {c['domain']}: {count} pages")
292
+
293
+ # Logic for Gaps vs Strengths
294
+ gap_html = ""
295
+ strength_html = ""
296
 
297
+ # GAP DETECTION
298
+ if comp_matches_total > 0 and user_count == 0:
299
+ gap_html = f"""
300
+ <div style="background-color: #fff4f4; border-left: 5px solid #ef4444; padding: 15px; border-radius: 5px;">
301
+ <h3 style="color: #ef4444; margin: 0;">🔴 CRITICAL GAP: "{topic}"</h3>
302
+ <p><strong>Competitors are winning here.</strong> You have 0 pages, while competitors have {comp_matches_total}.</p>
303
+ <p><em>Competitor Coverage:</em></p>
304
+ <pre>{chr(10).join(comp_details)}</pre>
305
+ </div>
306
+ """
307
+ elif comp_matches_total > user_count * 2:
308
+ gap_html = f"""
309
+ <div style="background-color: #fffbf0; border-left: 5px solid #f59e0b; padding: 15px; border-radius: 5px;">
310
+ <h3 style="color: #f59e0b; margin: 0;">🟡 WEAKNESS: "{topic}"</h3>
311
+ <p><strong>You are falling behind.</strong> Competitors have {comp_matches_total} pages vs your {user_count}.</p>
312
+ </div>
313
+ """
314
+ else:
315
+ gap_html = f"""
316
+ <div style="background-color: #f0fdf4; border-left: 5px solid #22c55e; padding: 15px; border-radius: 5px;">
317
+ <h3 style="color: #22c55e; margin: 0;">✅ NO GAP: "{topic}"</h3>
318
+ <p>You are covering this topic effectively compared to competitors.</p>
319
+ </div>
320
+ """
321
+
322
+ # STRENGTH DETECTION
323
+ if user_count > 0 and comp_matches_total == 0:
324
+ strength_html = f"""
325
+ <div style="background-color: #f0fdf4; border-left: 5px solid #22c55e; padding: 15px; border-radius: 5px;">
326
+ <h3 style="color: #22c55e; margin: 0;">💎 UNIQUE STRENGTH: "{topic}"</h3>
327
+ <p><strong>Blue Ocean Opportunity!</strong> You have {user_count} pages, and competitors have NONE.</p>
328
+ <p><em>Action:</em> Double down to own this niche completely.</p>
329
+ </div>
330
+ """
331
+ elif user_count > comp_matches_total:
332
+ strength_html = f"""
333
+ <div style="background-color: #f0fdf4; border-left: 5px solid #22c55e; padding: 15px; border-radius: 5px;">
334
+ <h3 style="color: #22c55e; margin: 0;">💪 LEADING: "{topic}"</h3>
335
+ <p>You are the market leader for this topic with {user_count} pages (Competitors avg: {comp_matches_total}).</p>
336
+ </div>
337
+ """
338
+ else:
339
+ strength_html = f"""
340
+ <div style="padding: 15px; color: #666;">
341
+ <p>No unique strength detected for "{topic}" yet.</p>
342
+ </div>
343
+ """
344
+
345
+ return gap_html, strength_html
346
+
347
+ def generate_radar(user_urls, competitors):
348
+ fig = go.Figure()
349
+ cats = ['TOFU', 'MOFU', 'BOFU']
350
+ if user_urls:
351
+ stages = [classify_stage(u) for u in user_urls]
352
+ tot = len(stages) or 1
353
+ fig.add_trace(go.Scatterpolar(r=[(stages.count(s)/tot)*100 for s in cats], theta=cats, fill='toself', name='You'))
354
+
355
+ for c in competitors:
356
+ s = c['stats']
357
+ fig.add_trace(go.Scatterpolar(r=[s['TOFU'], s['MOFU'], s['BOFU']], theta=cats, fill='toself', name=c['domain']))
358
+
359
+ fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), title="Strategy Radar")
360
+ return fig
361
 
362
  # ============================================================================
363
+ # 6. UI HANDLING
364
  # ============================================================================
365
 
366
  state = {'urls': [], 'categories': {}, 'competitors': []}
 
369
  urls, msg = fetch_sitemap_links(url)
370
  cats = categorize_links(urls)
371
  cat_list = ["All"] + sorted(list(cats.keys()))
372
+ return urls, cats, msg, gr.update(choices=cat_list, value="All"), gr.update(choices=[])
373
 
374
  def ui_update_topics(category, categories_data):
375
  if not categories_data: return gr.update(choices=[])
 
378
  return gr.update(choices=all_topics)
379
  return gr.update(choices=sorted(categories_data.get(category, [])))
380
 
381
+ def ui_analyze_planner(urls, category, topic, mode, syns, negs):
382
+ if not urls: return "Fetch data first.", None, None
383
+ if not topic: return "Please select or enter a topic.", None, None
384
+
385
+ matched = match_topic_urls(urls, topic, category, mode, syns, negs)
386
+ stages = [classify_stage(u) for u in matched]
387
+ total = len(stages) or 1
388
+ tofu = (stages.count('TOFU')/total)*100
389
+ mofu = (stages.count('MOFU')/total)*100
390
+ bofu = (stages.count('BOFU')/total)*100
391
+
392
+ summary = f"### Analysis: {topic}\n**Matched Pages:** {len(matched)}\n\n"
393
+ summary += f"**Funnel Health:**\n- TOFU: {tofu:.1f}%\n- MOFU: {mofu:.1f}%\n- BOFU: {bofu:.1f}%\n\n"
394
+ return summary, matched, None
395
+
396
  def ui_add_comp(url, current_comps):
397
  urls, msg = fetch_sitemap_links(url)
398
  domain = urlparse(url if url.startswith('http') else f'https://{url}').netloc
 
405
  txt = "\n".join([f"✅ {c['domain']}: {c['stats']['Count']} pages" for c in current_comps])
406
  return current_comps, txt, ""
407
 
408
+ def ui_analyze_comp_topic(user_urls, competitors, topic):
409
+ """
410
+ Triggered when user types a topic in Competitor tab.
411
+ Calculates specific Gaps & Strengths for that topic.
412
+ """
413
+ if not user_urls: return None, "Fetch Data First", "Fetch Data First"
 
 
 
 
 
 
 
 
414
 
415
+ radar = generate_radar(user_urls, competitors)
416
+ gap_html, strength_html = analyze_topic_gap_strength(user_urls, competitors, topic)
 
417
 
418
+ return radar, gap_html, strength_html
 
419
 
420
  # ============================================================================
421
  # LAYOUT
 
426
  state_cats_ui = gr.State({})
427
  state_comps_ui = gr.State([])
428
 
429
+ gr.Markdown("# 🚀 B2B Content Strategy & Multi-Model Intelligence")
430
 
431
  with gr.Tabs():
432
  # TAB 1: PLANNER
 
440
  cat_dd = gr.Dropdown(label="Category", choices=[], allow_custom_value=True)
441
  topic_dd = gr.Dropdown(label="Topic", choices=[], allow_custom_value=True)
442
 
443
+ with gr.Accordion("Advanced Filters", open=False):
 
444
  match_mode = gr.Radio(["fuzzy", "exact"], label="Mode", value="fuzzy")
445
+ synonyms = gr.Textbox(label="Synonyms")
446
  neg_kw = gr.Textbox(label="Negative Keywords")
447
 
448
+ analyze_btn = gr.Button("Analyze Content", variant="secondary")
449
 
450
  with gr.Row():
451
+ plan_summary = gr.Markdown()
452
  plan_urls = gr.Textbox(label="Matched URLs", lines=5)
 
453
 
454
+ gr.Markdown("### ✨ Multi-Model AI Idea Generator")
455
+ gr.Markdown("Generating 10 structured ideas using **Strategist, Creative, and SEO Expert** agents.")
456
+ gen_btn = gr.Button("🚀 Generate 10 SEO-Optimized Ideas", variant="primary")
457
 
458
+ idea_out = gr.Markdown()
 
 
459
 
460
  # TAB 2: COMPETITORS
461
  with gr.Tab("🏆 Competitor Intelligence"):
 
464
  add_comp = gr.Button("Add Competitor")
465
  comp_display = gr.Markdown("No competitors added.")
466
 
467
+ gr.Markdown("### 🕵️ Topic Gap & Strength Analysis")
468
+ gr.Markdown("Type a topic below to see where you are winning or losing against competitors.")
469
+
470
  with gr.Row():
471
+ # User requested: "When I type topic its provide Content Gaps... and Your Content Strengths"
472
+ target_topic = gr.Textbox(label="Enter Topic to Analyze (e.g., 'Generative AI')")
473
+ comp_analyze = gr.Button("Analyze Topic", variant="primary")
474
 
475
  with gr.Row():
476
  radar_plot = gr.Plot(label="Strategy Radar")
 
477
 
478
  with gr.Row():
479
+ # Using HTML for rich formatting of Gaps/Strengths
480
+ gap_res = gr.HTML(label="Content Gaps (Opportunities)")
481
+ str_res = gr.HTML(label="Your Content Strengths")
482
 
483
  # EVENTS
484
  fetch_btn.click(ui_fetch, [url_input], [state_urls_ui, state_cats_ui, fetch_log, cat_dd, topic_dd])
485
  cat_dd.change(ui_update_topics, [cat_dd, state_cats_ui], [topic_dd])
 
 
486
  analyze_btn.click(ui_analyze_planner, [state_urls_ui, cat_dd, topic_dd, match_mode, synonyms, neg_kw], [plan_summary, plan_urls, gr.State()])
487
 
488
+ # Generate Multi-Model Ideas
489
+ gen_btn.click(ui_generate_multi_model_ideas, [topic_dd, gr.State("TOFU")], [idea_out])
490
 
491
  add_comp.click(ui_add_comp, [comp_url, state_comps_ui], [state_comps_ui, comp_display, comp_url])
492
 
493
+ # Analyze Specific Topic Gaps/Strengths
494
+ comp_analyze.click(ui_analyze_comp_topic,
495
+ [state_urls_ui, state_comps_ui, target_topic],
496
+ [radar_plot, gap_res, str_res])
497
 
498
  if __name__ == "__main__":
499
  demo.launch()