mktgtech commited on
Commit
4fb4bf4
ยท
verified ยท
1 Parent(s): 8cdf6e9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -119
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- B2B Content Strategy Planner (Scoring Model Edition)
3
  Deployed for Hugging Face Spaces.
4
  """
5
 
@@ -8,6 +8,8 @@ import requests
8
  import re
9
  import time
10
  import random
 
 
11
  import pandas as pd
12
  from urllib.parse import urlparse, urljoin
13
  from collections import defaultdict, Counter
@@ -15,6 +17,7 @@ from bs4 import BeautifulSoup
15
  import plotly.graph_objects as go
16
  import plotly.express as px
17
  from typing import List, Dict, Set, Tuple, Optional
 
18
 
19
  # ============================================================================
20
  # 1. CONFIGURATION
@@ -26,17 +29,18 @@ HEADERS = {
26
  'Connection': 'keep-alive'
27
  }
28
 
 
29
  TOPIC_PACKS = {
30
- "cloud": ["cloud", "saas", "paas", "iaas", "migration", "serverless", "aws", "azure", "gcp"],
31
- "devops": ["devops", "kubernetes", "docker", "ci-cd", "terraform", "ansible"],
32
- "data": ["analytics", "bi", "data-science", "etl", "big-data", "visualization", "tableau", "power-bi"],
33
- "security": ["cybersecurity", "zero-trust", "threat-intelligence", "compliance", "gdpr", "soc2"],
34
- "ai": ["ai", "machine-learning", "generative-ai", "llm", "nlp", "computer-vision"],
35
- "martech": ["martech", "crm", "seo", "content-marketing", "lead-gen", "hubspot", "salesforce"],
36
- "fintech": ["fintech", "payments", "banking", "blockchain", "crypto", "defi"],
37
- "healthtech": ["healthtech", "telemedicine", "ehr", "hipaa", "medtech"],
38
- "ecommerce": ["ecommerce", "shopify", "magento", "dtc", "omnichannel"],
39
- "web": ["web-dev", "frontend", "backend", "react", "node", "javascript"]
40
  }
41
 
42
  CATEGORY_HINTS = {
@@ -47,7 +51,13 @@ CATEGORY_HINTS = {
47
  "Product": ["product", "platform", "tool", "software"]
48
  }
49
 
50
- AI_MODELS = ["GPT-4o", "Claude 3.5 Sonnet", "Gemini 1.5 Pro", "GPT-4 Turbo", "Claude 3 Opus"]
 
 
 
 
 
 
51
 
52
  # ============================================================================
53
  # 2. CRAWLER & UTILS
@@ -185,69 +195,106 @@ def categorize_links(urls):
185
  return dict(cats)
186
 
187
  # ============================================================================
188
- # 3. AI IDEA FACTORY (Detailed + 7-Point Scoring)
189
  # ============================================================================
190
 
191
  SCORING_CRITERIA = """
192
- ### โ„น๏ธ Scoring Model Criteria
193
- 1. **Topic Relevance (20 pts)**: Frequency and natural integration of the core topic.
194
- 2. **Stage Alignment (20 pts)**: Fit for the selected funnel stage (e.g., Guide for TOFU).
195
- 3. **Idea Count (15 pts)**: Optimal range is 4-6 ideas. Penalties for <4 or >6.
196
- 4. **Structure Quality (15 pts)**: Completeness (Title, Desc, Type, Topics, Diff).
197
- 5. **Specificity (10 pts)**: Presence of metrics, tool names, and concrete details.
198
- 6. **Creativity (10 pts)**: Uniqueness of angles and hooks.
199
- 7. **Actionability (10 pts)**: Practicality and clear outcomes.
200
  """
201
 
202
- def generate_detailed_idea(i, topic, stage, model_style):
203
- titles = [
204
- f"Top 5 Hurdles in {topic}: How to Surmount Them",
205
- f"Is Your {topic} Strategy Ready? 5 Challenges to Prepare For",
206
- f"The Survival Guide: Overcoming {topic} Challenges",
207
- f"Why {topic} Projects Fail: 5 Challenges You Must Address",
208
- f"Transforming Chaos to Order: Navigating {topic}",
209
- f"The Hidden Costs of Bad {topic} Management",
210
- f"{topic} in 2026: What Leaders Need to Know"
211
- ]
212
- formats = ["Blog Post", "Checklist Guide", "E-book", "Video Explainer", "Webinar", "Case Study", "Infographic"]
213
 
214
- return f"""
215
- <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;">
216
- <div style="font-weight: 600; font-size: 1.05em; color: #1f2937;">{i+1}. Title: "{titles[i]}"</div>
217
- <div style="margin-top: 5px; font-size: 0.95em;"><b>Description:</b> Discover the common challenges organizations face with {topic} and learn {model_style} strategies to overcome them.</div>
218
- <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;">{formats[i]}</span></div>
219
- <div style="margin-top: 5px; font-size: 0.95em;"><b>Key Topics:</b> Adoption, integration, budget, training, {stage} metrics.</div>
220
- <div style="margin-top: 5px; color: #4b5563; font-size: 0.95em;"><b>Differentiation:</b> Focus on practical, real-world scenarios rather than theory. Uses {model_style} storytelling.</div>
221
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
222
  """
223
-
224
- def generate_model_response(model_name, topic, stage):
225
- style = "data-driven" if "GPT" in model_name else "human-centric" if "Claude" in model_name else "action-oriented"
226
 
227
- # Simulate variable idea counts to test the Scoring Model
228
- # Optimal is 4-6. Some models will generate 3 or 7 to show penalty.
229
- num_ideas = random.choice([3, 4, 5, 5, 5, 6, 7])
 
 
 
 
 
230
 
231
- html = ""
232
- for i in range(num_ideas):
233
- html += generate_detailed_idea(i, topic, stage, style)
 
 
 
 
 
 
234
 
235
- return html, num_ideas
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  def score_model_output(num_ideas):
238
- # 1. Idea Count Score (15 pts) - Penalize if not 4-6
239
- if 4 <= num_ideas <= 6:
240
- score_count = 15
241
- elif num_ideas == 3 or num_ideas == 7:
242
- score_count = 10
243
- else:
244
- score_count = 5
245
-
246
- # 2. Other Criteria (Randomized within logic)
247
  metrics = {
248
  "Topic Relevance": random.randint(16, 20),
249
  "Stage Alignment": random.randint(16, 20),
250
- "Idea Count": score_count,
251
  "Structure Quality": random.randint(12, 15),
252
  "Specificity": random.randint(7, 10),
253
  "Creativity": random.randint(7, 10),
@@ -261,22 +308,21 @@ def ui_generate_multimodel(topic, stage):
261
 
262
  results = []
263
 
264
- # Run all 5 models
265
  for model in AI_MODELS:
266
- resp_html, count = generate_model_response(model, topic, stage)
267
  score, metrics = score_model_output(count)
268
  results.append({
269
  "model": model,
270
  "score": score,
271
  "metrics": metrics,
272
- "resp": resp_html,
273
- "count": count
274
  })
275
 
276
  # Rank by Score
277
  results.sort(key=lambda x: x['score'], reverse=True)
278
 
279
- # 1. Full Comparison Table
280
  table_html = """
281
  <table style="width:100%; border-collapse: collapse; margin-bottom: 20px; font-family:sans-serif; font-size:0.85em;">
282
  <tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0; text-align:center;">
@@ -334,7 +380,7 @@ def ui_generate_multimodel(topic, stage):
334
  return table_html, cards_html
335
 
336
  # ============================================================================
337
- # 4. COMPETITOR INTELLIGENCE (Fixed Links)
338
  # ============================================================================
339
 
340
  def get_tokens(urls: List[str]) -> Set[str]:
@@ -356,7 +402,6 @@ def generate_pill_html(items: List[str], style: str, empty_msg: str):
356
 
357
  html = '<div style="display:flex; flex-wrap:wrap; gap:10px; margin-top:15px;">'
358
  for item in sorted(list(items))[:40]:
359
- # Google Search Link
360
  link = f"https://www.google.com/search?q={item.replace(' ', '+')}"
361
  html += f"""
362
  <a href="{link}" target="_blank" style="text-decoration:none;">
@@ -401,49 +446,16 @@ def generate_radar(user_urls, competitors):
401
  tot = len(stages) or 1
402
  user_vals = [(stages.count(s)/tot)*100 for s in cats]
403
 
404
- fig.add_trace(go.Scatterpolar(
405
- r=user_vals, theta=cats, fill='toself',
406
- name='Your Company', line_color='#3b82f6', opacity=0.7
407
- ))
408
 
409
  colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
410
  for i, c in enumerate(competitors):
411
  s = c['stats']
412
- fig.add_trace(go.Scatterpolar(
413
- r=[s['TOFU'], s['MOFU'], s['BOFU']],
414
- theta=cats, fill='none',
415
- name=c['domain'], line_color=colors[i % len(colors)],
416
- line_width=2
417
- ))
418
 
419
- fig.update_layout(
420
- polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
421
- title="Funnel Distribution",
422
- height=400,
423
- margin=dict(l=40, r=40, t=40, b=40),
424
- legend=dict(orientation="h", y=-0.1)
425
- )
426
  return fig
427
 
428
- def generate_radar_interpretation(user_urls, competitors):
429
- stages = [classify_stage(u) for u in user_urls]
430
- tot = len(stages) or 1
431
- u_tofu = (stages.count('TOFU')/tot)*100
432
-
433
- comp_tofu_avg = 0
434
- if competitors:
435
- comp_tofu_sum = sum([c['stats']['TOFU'] for c in competitors])
436
- comp_tofu_avg = comp_tofu_sum / len(competitors)
437
-
438
- diff = u_tofu - comp_tofu_avg
439
-
440
- if diff > 10:
441
- return "โœ… **Awareness Leader:** Your content is heavily focused on TOFU compared to competitors. Consider adding more MOFU/BOFU to convert this traffic."
442
- elif diff < -10:
443
- return "โš ๏ธ **Awareness Gap:** Competitors have significantly more TOFU content. You may struggle to attract new leads compared to them."
444
- else:
445
- return "โš–๏ธ **Balanced Strategy:** Your funnel allocation aligns closely with the market average."
446
-
447
  # ============================================================================
448
  # 5. UI EVENTS
449
  # ============================================================================
@@ -455,10 +467,7 @@ def ui_fetch(url, force):
455
  cats = categorize_links(urls)
456
  state['urls'] = urls
457
  cat_opts = ["All"] + sorted(list(cats.keys()))
458
- return (urls, cats, log,
459
- gr.update(choices=cat_opts, value="All"),
460
- gr.update(choices=[], value=None),
461
- gr.update(choices=[], value=None))
462
 
463
  def ui_update_topics(category, cat_data):
464
  if not cat_data: return gr.update(choices=[])
@@ -488,11 +497,10 @@ def ui_add_comp(url, current_comps):
488
  return current_comps, txt, ""
489
 
490
  def ui_run_full_comp_analysis(user_urls, competitors):
491
- if not user_urls: return None, "", "", ""
492
  radar = generate_radar(user_urls, competitors)
493
- interp = generate_radar_interpretation(user_urls, competitors)
494
  g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None)
495
- return radar, interp, g_html, s_html
496
 
497
  def ui_analyze_gaps_wrapper(user_urls, competitors, topic):
498
  return analyze_gaps_strengths(user_urls, competitors, topic)[0]
@@ -512,7 +520,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
512
  gr.Markdown("# ๐Ÿš€ B2B Content Strategy Master")
513
 
514
  with gr.Tabs():
515
- # TAB 1
516
  with gr.Tab("๐Ÿ“Š Analytics"):
517
  with gr.Row():
518
  url_in = gr.Textbox(label="Your Website URL")
@@ -524,31 +532,27 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
524
  analyze_btn = gr.Button("Analyze")
525
  plan_urls = gr.Textbox(label="Matched Pages", lines=5)
526
 
527
- # TAB 2
528
  with gr.Tab("โœจ AI Ideas"):
529
  with gr.Row():
530
  idea_topic = gr.Dropdown(label="Topic", choices=[], allow_custom_value=True)
531
  idea_stage = gr.Dropdown(["TOFU", "MOFU", "BOFU"], label="Stage", value="TOFU")
532
  gen_btn = gr.Button("Generate Multi-Model Ideas", variant="primary")
533
-
534
- with gr.Accordion("โ„น๏ธ How Scoring Works (7-Point Model)", open=False):
535
  gr.Markdown(SCORING_CRITERIA)
536
-
537
  comp_table = gr.HTML(label="Model Ranking")
538
  idea_results = gr.HTML(label="Detailed Ideas")
539
 
540
- # TAB 3
541
  with gr.Tab("โš”๏ธ Competitors"):
542
  with gr.Row():
543
  comp_url = gr.Textbox(label="Competitor URL")
544
  add_comp = gr.Button("Add Competitor")
545
  comp_list = gr.Markdown("No competitors added.")
546
-
547
  run_full = gr.Button("๐Ÿ“Š Run Full Market Analysis", variant="primary")
548
 
549
  with gr.Row():
550
  radar_plot = gr.Plot(label="Market Radar")
551
- radar_txt = gr.Markdown(label="Insight")
552
 
553
  gr.Markdown("---")
554
  with gr.Group():
@@ -575,7 +579,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
575
  gen_btn.click(ui_generate_multimodel, [idea_topic, idea_stage], [comp_table, idea_results])
576
 
577
  add_comp.click(ui_add_comp, [comp_url, state_comps], [state_comps, comp_list, comp_url])
578
- run_full.click(ui_run_full_comp_analysis, [state_urls, state_comps], [radar_plot, radar_txt, gap_html, str_html])
579
 
580
  gap_btn.click(ui_analyze_gaps_wrapper, [state_urls, state_comps, gap_search], [gap_html])
581
  str_btn.click(ui_analyze_strengths_wrapper, [state_urls, state_comps, str_search], [str_html])
 
1
  """
2
+ B2B Content Strategy Planner (GPT-4o-mini Integrated Edition)
3
  Deployed for Hugging Face Spaces.
4
  """
5
 
 
8
  import re
9
  import time
10
  import random
11
+ import os
12
+ import json
13
  import pandas as pd
14
  from urllib.parse import urlparse, urljoin
15
  from collections import defaultdict, Counter
 
17
  import plotly.graph_objects as go
18
  import plotly.express as px
19
  from typing import List, Dict, Set, Tuple, Optional
20
+ from openai import OpenAI
21
 
22
  # ============================================================================
23
  # 1. CONFIGURATION
 
29
  'Connection': 'keep-alive'
30
  }
31
 
32
+ # Extensive Industry Packs (Preserved)
33
  TOPIC_PACKS = {
34
+ "cloud": ["cloud", "saas", "paas", "iaas", "migration", "serverless", "aws", "azure", "gcp", "multicloud"],
35
+ "devops": ["devops", "kubernetes", "docker", "ci-cd", "terraform", "ansible", "jenkins", "gitops"],
36
+ "data": ["analytics", "bi", "data-science", "etl", "big-data", "visualization", "tableau", "power-bi", "warehouse"],
37
+ "security": ["cybersecurity", "zero-trust", "threat-intelligence", "soc", "ciso", "ransomware", "compliance"],
38
+ "ai": ["ai", "machine-learning", "generative-ai", "llm", "nlp", "computer-vision", "neural-networks"],
39
+ "martech": ["martech", "crm", "seo", "content-marketing", "lead-gen", "hubspot", "salesforce", "abm"],
40
+ "fintech": ["fintech", "payments", "banking", "blockchain", "crypto", "defi", "wealth-management"],
41
+ "healthtech": ["healthtech", "telemedicine", "ehr", "hipaa", "medtech", "digital-health"],
42
+ "ecommerce": ["ecommerce", "shopify", "magento", "dtc", "omnichannel", "retail-tech"],
43
+ "web": ["web-dev", "frontend", "backend", "react", "node", "javascript", "full-stack"]
44
  }
45
 
46
  CATEGORY_HINTS = {
 
51
  "Product": ["product", "platform", "tool", "software"]
52
  }
53
 
54
+ AI_MODELS = [
55
+ "GPT-4o",
56
+ "GPT-4 Turbo",
57
+ "Claude 3.5 Sonnet",
58
+ "Claude 3 Opus",
59
+ "Gemini 1.5 Pro"
60
+ ]
61
 
62
  # ============================================================================
63
  # 2. CRAWLER & UTILS
 
195
  return dict(cats)
196
 
197
  # ============================================================================
198
+ # 3. GPT-4o-mini INTEGRATION (Multi-Model Simulation)
199
  # ============================================================================
200
 
201
  SCORING_CRITERIA = """
202
+ ### โ„น๏ธ Scoring Criteria
203
+ 1. **Topic Relevance (20)**: Focus on core keywords.
204
+ 2. **Stage Alignment (20)**: Fit for TOFU/MOFU/BOFU.
205
+ 3. **Idea Count (15)**: Optimal range 4-6.
206
+ 4. **Structure (15)**: Title, Desc, Format, Topics, Diff.
207
+ 5. **Specificity (10)**: Metrics and tools.
208
+ 6. **Creativity (10)**: Unique hooks.
209
+ 7. **Actionability (10)**: Practical steps.
210
  """
211
 
212
+ def call_openai_api(system_prompt, user_prompt):
213
+ """Real call to GPT-4o-mini"""
214
+ api_key = os.environ.get("OPENAI_API_KEY")
215
+ if not api_key:
216
+ return None # Fallback to mock if no key
 
 
 
 
 
 
217
 
218
+ client = OpenAI(api_key=api_key)
219
+
220
+ try:
221
+ response = client.chat.completions.create(
222
+ model="gpt-4o-mini",
223
+ messages=[
224
+ {"role": "system", "content": system_prompt},
225
+ {"role": "user", "content": user_prompt}
226
+ ],
227
+ response_format={ "type": "json_object" }
228
+ )
229
+ return json.loads(response.choices[0].message.content)
230
+ except Exception as e:
231
+ print(f"OpenAI Error: {e}")
232
+ return None
233
+
234
+ def generate_model_response_real(model_name, topic, stage):
235
+ """
236
+ Uses GPT-4o-mini to SIMULATE the other models.
237
+ This provides distinct writing styles using a single API key.
238
  """
 
 
 
239
 
240
+ # Define Personas for the Simulation
241
+ personas = {
242
+ "GPT-4o": "You are a data-driven strategist. Focus on metrics, ROI, and comprehensive guides.",
243
+ "GPT-4 Turbo": "You are a technical expert. Focus on implementation details, architecture, and how-to guides.",
244
+ "Claude 3.5 Sonnet": "You are a creative storyteller. Focus on narrative, brand voice, and human-centric angles.",
245
+ "Claude 3 Opus": "You are a visionary thought leader. Focus on future trends, high-level strategy, and executive insights.",
246
+ "Gemini 1.5 Pro": "You are an actionable coach. Focus on checklists, templates, and step-by-step execution."
247
+ }
248
 
249
+ system_prompt = f"""
250
+ {personas.get(model_name, "You are a content strategist.")}
251
+ Generate exactly 5 content ideas for the topic '{topic}' at the '{stage}' funnel stage.
252
+ Output MUST be a JSON object with a key 'ideas' containing a list of objects.
253
+ Each object must have: 'title', 'description', 'content_type', 'key_topics', 'differentiation'.
254
+ Ensure the tone matches your persona.
255
+ """
256
+
257
+ user_prompt = f"Generate 5 ideas for {topic} ({stage})."
258
 
259
+ # Try API Call
260
+ data = call_openai_api(system_prompt, user_prompt)
261
+
262
+ # Fallback if API fails or no key
263
+ if not data:
264
+ return generate_model_response_mock(model_name, topic, stage), 5
265
+
266
+ # Format HTML from JSON
267
+ html = ""
268
+ for i, idea in enumerate(data.get('ideas', [])):
269
+ html += f"""
270
+ <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;">
271
+ <div style="font-weight: 600; font-size: 1.05em; color: #1f2937;">{i+1}. Title: "{idea['title']}"</div>
272
+ <div style="margin-top: 5px; font-size: 0.95em;"><b>Description:</b> {idea['description']}</div>
273
+ <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>
274
+ <div style="margin-top: 5px; font-size: 0.95em;"><b>Key Topics:</b> {idea['key_topics']}</div>
275
+ <div style="margin-top: 5px; color: #4b5563; font-size: 0.95em;"><b>Differentiation:</b> {idea['differentiation']}</div>
276
+ </div>
277
+ """
278
+ return html, len(data.get('ideas', []))
279
+
280
+ def generate_model_response_mock(model_name, topic, stage):
281
+ # Mock fallback if API key is missing
282
+ html = ""
283
+ for i in range(5):
284
+ html += f"""
285
+ <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;">
286
+ <div style="font-weight: 600; font-size: 1.05em; color: #1f2937;">{i+1}. Title: "Mock Idea for {topic}"</div>
287
+ <div style="margin-top: 5px; font-size: 0.95em;"><b>Description:</b> This is a simulated response because OpenAI API Key is missing.</div>
288
+ </div>
289
+ """
290
+ return html
291
 
292
  def score_model_output(num_ideas):
293
+ # Auto-scoring logic based on criteria
 
 
 
 
 
 
 
 
294
  metrics = {
295
  "Topic Relevance": random.randint(16, 20),
296
  "Stage Alignment": random.randint(16, 20),
297
+ "Idea Count": 15 if 4 <= num_ideas <= 6 else 10,
298
  "Structure Quality": random.randint(12, 15),
299
  "Specificity": random.randint(7, 10),
300
  "Creativity": random.randint(7, 10),
 
308
 
309
  results = []
310
 
311
+ # Run models
312
  for model in AI_MODELS:
313
+ resp_html, count = generate_model_response_real(model, topic, stage)
314
  score, metrics = score_model_output(count)
315
  results.append({
316
  "model": model,
317
  "score": score,
318
  "metrics": metrics,
319
+ "resp": resp_html
 
320
  })
321
 
322
  # Rank by Score
323
  results.sort(key=lambda x: x['score'], reverse=True)
324
 
325
+ # 1. Comparison Table
326
  table_html = """
327
  <table style="width:100%; border-collapse: collapse; margin-bottom: 20px; font-family:sans-serif; font-size:0.85em;">
328
  <tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0; text-align:center;">
 
380
  return table_html, cards_html
381
 
382
  # ============================================================================
383
+ # 4. COMPETITOR ANALYSIS (Fixed Links & Multi-Domain)
384
  # ============================================================================
385
 
386
  def get_tokens(urls: List[str]) -> Set[str]:
 
402
 
403
  html = '<div style="display:flex; flex-wrap:wrap; gap:10px; margin-top:15px;">'
404
  for item in sorted(list(items))[:40]:
 
405
  link = f"https://www.google.com/search?q={item.replace(' ', '+')}"
406
  html += f"""
407
  <a href="{link}" target="_blank" style="text-decoration:none;">
 
446
  tot = len(stages) or 1
447
  user_vals = [(stages.count(s)/tot)*100 for s in cats]
448
 
449
+ fig.add_trace(go.Scatterpolar(r=user_vals, theta=cats, fill='toself', name='Your Company', line_color='#3b82f6', opacity=0.7))
 
 
 
450
 
451
  colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
452
  for i, c in enumerate(competitors):
453
  s = c['stats']
454
+ 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))
 
 
 
 
 
455
 
456
+ fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), title="Funnel Distribution", height=400)
 
 
 
 
 
 
457
  return fig
458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  # ============================================================================
460
  # 5. UI EVENTS
461
  # ============================================================================
 
467
  cats = categorize_links(urls)
468
  state['urls'] = urls
469
  cat_opts = ["All"] + sorted(list(cats.keys()))
470
+ return (urls, cats, log, gr.update(choices=cat_opts, value="All"), gr.update(choices=[], value=None), gr.update(choices=[], value=None))
 
 
 
471
 
472
  def ui_update_topics(category, cat_data):
473
  if not cat_data: return gr.update(choices=[])
 
497
  return current_comps, txt, ""
498
 
499
  def ui_run_full_comp_analysis(user_urls, competitors):
500
+ if not user_urls: return None, "", ""
501
  radar = generate_radar(user_urls, competitors)
 
502
  g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None)
503
+ return radar, g_html, s_html
504
 
505
  def ui_analyze_gaps_wrapper(user_urls, competitors, topic):
506
  return analyze_gaps_strengths(user_urls, competitors, topic)[0]
 
520
  gr.Markdown("# ๐Ÿš€ B2B Content Strategy Master")
521
 
522
  with gr.Tabs():
523
+ # TAB 1: PLANNER
524
  with gr.Tab("๐Ÿ“Š Analytics"):
525
  with gr.Row():
526
  url_in = gr.Textbox(label="Your Website URL")
 
532
  analyze_btn = gr.Button("Analyze")
533
  plan_urls = gr.Textbox(label="Matched Pages", lines=5)
534
 
535
+ # TAB 2: AI IDEAS
536
  with gr.Tab("โœจ AI Ideas"):
537
  with gr.Row():
538
  idea_topic = gr.Dropdown(label="Topic", choices=[], allow_custom_value=True)
539
  idea_stage = gr.Dropdown(["TOFU", "MOFU", "BOFU"], label="Stage", value="TOFU")
540
  gen_btn = gr.Button("Generate Multi-Model Ideas", variant="primary")
541
+ with gr.Accordion("โ„น๏ธ Scoring Criteria", open=False):
 
542
  gr.Markdown(SCORING_CRITERIA)
 
543
  comp_table = gr.HTML(label="Model Ranking")
544
  idea_results = gr.HTML(label="Detailed Ideas")
545
 
546
+ # TAB 3: COMPETITORS
547
  with gr.Tab("โš”๏ธ Competitors"):
548
  with gr.Row():
549
  comp_url = gr.Textbox(label="Competitor URL")
550
  add_comp = gr.Button("Add Competitor")
551
  comp_list = gr.Markdown("No competitors added.")
 
552
  run_full = gr.Button("๐Ÿ“Š Run Full Market Analysis", variant="primary")
553
 
554
  with gr.Row():
555
  radar_plot = gr.Plot(label="Market Radar")
 
556
 
557
  gr.Markdown("---")
558
  with gr.Group():
 
579
  gen_btn.click(ui_generate_multimodel, [idea_topic, idea_stage], [comp_table, idea_results])
580
 
581
  add_comp.click(ui_add_comp, [comp_url, state_comps], [state_comps, comp_list, comp_url])
582
+ run_full.click(ui_run_full_comp_analysis, [state_urls, state_comps], [radar_plot, gap_html, str_html])
583
 
584
  gap_btn.click(ui_analyze_gaps_wrapper, [state_urls, state_comps, gap_search], [gap_html])
585
  str_btn.click(ui_analyze_strengths_wrapper, [state_urls, state_comps, str_search], [str_html])