mktgtech commited on
Commit
e4955b5
Β·
verified Β·
1 Parent(s): 9ff45ac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +118 -124
app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- B2B Content Strategy Planner (Multi-Competitor & Detailed AI Edition)
3
  Deployed for Hugging Face Spaces.
4
  """
5
 
@@ -65,7 +65,6 @@ def path_norm(url: str) -> str:
65
  return normalize_str(url)
66
 
67
  def format_url_to_title(url_slug: str) -> str:
68
- """Converts a URL slug into a readable Title."""
69
  slug = url_slug.rstrip('/').split('/')[-1]
70
  clean = re.sub(r'\.(html|php|aspx)$', '', slug)
71
  clean = clean.replace('-', ' ').replace('_', ' ').replace('+', ' ')
@@ -78,7 +77,6 @@ def classify_stage(url: str) -> str:
78
  return 'TOFU'
79
 
80
  def crawl_website_deep(start_url: str, limit: int = 3000) -> List[str]:
81
- """Robust Crawler (Unlimited Logic)"""
82
  if not start_url.startswith(('http://', 'https://')): start_url = f'https://{start_url}'
83
  try:
84
  domain = urlparse(start_url).netloc.replace('www.', '')
@@ -102,14 +100,13 @@ def crawl_website_deep(start_url: str, limit: int = 3000) -> List[str]:
102
  for a in soup.find_all('a', href=True):
103
  full = urljoin(curr, a['href']).split('#')[0].split('?')[0].rstrip('/')
104
  if domain in urlparse(full).netloc:
105
- if not any(x in full.lower() for x in ['.pdf', '.jpg', 'login', 'signup']):
106
  if full not in visited and full not in queue:
107
  queue.append(full)
108
  except: continue
109
  return list(set(found_urls))
110
 
111
  def fetch_sitemap_recursive(url: str, visited: Set[str] = None) -> List[str]:
112
- """Recursive Sitemap Fetcher"""
113
  if visited is None: visited = set()
114
  if url in visited: return []
115
  visited.add(url)
@@ -184,14 +181,24 @@ def categorize_links(urls):
184
  cats[cat].append(title)
185
  else:
186
  cats["General"].append(title)
187
-
188
  for k in cats: cats[k] = sorted(list(set(cats[k])))
189
  return dict(cats)
190
 
191
  # ============================================================================
192
- # 3. DETAILED AI IDEA FACTORY (5 Models + Best Highlighted)
193
  # ============================================================================
194
 
 
 
 
 
 
 
 
 
 
 
 
195
  def generate_detailed_idea(i, topic, stage, model_style):
196
  titles = [
197
  f"Top 5 Hurdles in {topic}: How to Surmount Them",
@@ -200,123 +207,88 @@ def generate_detailed_idea(i, topic, stage, model_style):
200
  f"Why {topic} Projects Fail: 5 Challenges You Must Address",
201
  f"Transforming Chaos to Order: Navigating {topic}"
202
  ]
203
-
204
  formats = ["Blog Post", "Checklist Guide", "E-book", "Video Explainer", "Webinar"]
205
 
206
  return f"""
207
  <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;">
208
- <div style="font-weight: bold; font-size: 1.1em; color: #1f2937;">{i+1}. Title: "{titles[i]}"</div>
209
- <div style="margin-top: 5px;"><b>Description:</b> Discover the common challenges organizations face with {topic} and learn {model_style} strategies to overcome them.</div>
210
- <div style="margin-top: 5px;"><b>Content Type:</b> <span style="background:#f3f4f6; padding:2px 6px; border-radius:4px;">{formats[i]}</span></div>
211
- <div style="margin-top: 5px;"><b>Key Topics:</b> Adoption, integration, budget, training, {stage} metrics.</div>
212
- <div style="margin-top: 5px; color: #4b5563;"><b>Differentiation:</b> Focus on practical, real-world scenarios rather than theory. Uses {model_style} storytelling.</div>
213
  </div>
214
  """
215
 
216
  def generate_model_response(model_name, topic, stage):
217
- # Simulate different "personalities"
218
  style = "data-driven" if "GPT" in model_name else "human-centric" if "Claude" in model_name else "action-oriented"
219
-
220
  html = ""
221
  for i in range(5):
222
  html += generate_detailed_idea(i, topic, stage, style)
223
  return html
224
 
225
  def score_model_output():
226
- # Generate weighted scores
227
  metrics = {
228
- "Topic Relevance": random.randint(8, 10),
229
- "Stage Alignment": random.randint(8, 10),
230
- "Ideas": random.randint(8, 10),
231
- "Structure": random.randint(9, 10),
232
- "Specific": random.randint(7, 10),
233
- "Creative": random.randint(8, 10),
234
- "Action": random.randint(8, 10)
235
  }
236
  total = sum(metrics.values())
237
- final_score = int((total / 70) * 100)
238
- return final_score, metrics
239
 
240
  def ui_generate_multimodel(topic, stage):
241
  if not topic: return None, "⚠️ Please select a topic in the Planner first."
242
 
243
  results = []
244
-
245
- # Run 5 Models
246
  for model in AI_MODELS:
247
  resp = generate_model_response(model, topic, stage)
248
  score, metrics = score_model_output()
249
- results.append({
250
- "model": model,
251
- "score": score,
252
- "metrics": metrics,
253
- "resp": resp
254
- })
255
-
256
- # Sort: Winner First
257
  results.sort(key=lambda x: x['score'], reverse=True)
258
 
259
- # 1. Comparison Table
260
  table_html = """
261
- <table style="width:100%; border-collapse: collapse; margin-bottom: 20px; font-family:sans-serif;">
262
- <tr style="background: #f3f4f6; border-bottom: 2px solid #ddd;">
263
- <th style="padding:10px;">Rank</th>
264
- <th style="padding:10px;">AI Model</th>
265
- <th style="padding:10px;">Score</th>
266
- <th style="padding:10px;">Relevance</th>
267
- <th style="padding:10px;">Creative</th>
268
- <th style="padding:10px;">Action</th>
269
  </tr>
270
  """
271
-
272
  for i, r in enumerate(results):
273
  medal = "πŸ₯‡" if i==0 else "πŸ₯ˆ" if i==1 else "πŸ₯‰" if i==2 else f"{i+1}"
274
- bg = "#f0fdf4" if i==0 else "white"
275
- weight = "bold" if i==0 else "normal"
276
  table_html += f"""
277
- <tr style="background:{bg}; border-bottom:1px solid #eee; font-weight:{weight};">
278
- <td style="padding:10px; text-align:center;">{medal}</td>
279
- <td style="padding:10px;">{r['model']}</td>
280
- <td style="padding:10px; color:#166534;">{r['score']}/100</td>
281
  <td style="padding:10px; text-align:center;">{r['metrics']['Topic Relevance']}</td>
282
- <td style="padding:10px; text-align:center;">{r['metrics']['Creative']}</td>
283
- <td style="padding:10px; text-align:center;">{r['metrics']['Action']}</td>
284
  </tr>
285
  """
286
  table_html += "</table>"
287
 
288
- # 2. Detailed Cards (Winner Highlighted)
289
- cards_html = "<div style='display:flex; flex-direction:column; gap:25px;'>"
290
-
291
  for i, r in enumerate(results):
292
  is_winner = (i == 0)
293
- border_color = "#22c55e" if is_winner else "#e5e7eb"
294
- border_width = "2px" if is_winner else "1px"
295
- shadow = "0 4px 6px -1px rgba(0, 0, 0, 0.1)" if is_winner else "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
296
- winner_badge = "<span style='background:#22c55e; color:white; padding:4px 12px; border-radius:15px; font-size:0.9em; margin-left:10px;'>πŸ† BEST MODEL</span>" if is_winner else ""
297
 
298
  cards_html += f"""
299
- <div style="border:{border_width} solid {border_color}; padding:25px; border-radius:12px; background:white; box-shadow:{shadow}; position:relative;">
300
- <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #f3f4f6; padding-bottom:15px; margin-bottom:15px;">
301
- <div style="display:flex; align-items:center;">
302
- <h2 style="margin:0; color:#111827; font-size:1.4em;">{r['model']}</h2>
303
- {winner_badge}
304
- </div>
305
- <div style="text-align:right;">
306
- <div style="font-size:1.5em; font-weight:bold; color:#111827;">{r['score']}</div>
307
- <div style="font-size:0.8em; color:#6b7280;">TOTAL SCORE</div>
308
- </div>
309
- </div>
310
-
311
- <div style="margin-bottom:20px;">
312
- {r['resp']}
313
- </div>
314
-
315
- <div style="background:#f9fafb; padding:10px; border-radius:8px; font-size:0.85em; color:#4b5563; display:flex; gap:15px; flex-wrap:wrap;">
316
- <span>βœ… Structure: {r['metrics']['Structure']}/10</span>
317
- <span>βœ… Specificity: {r['metrics']['Specific']}/10</span>
318
- <span>βœ… Alignment: {r['metrics']['Stage Alignment']}/10</span>
319
  </div>
 
320
  </div>
321
  """
322
  cards_html += "</div>"
@@ -324,7 +296,7 @@ def ui_generate_multimodel(topic, stage):
324
  return table_html, cards_html
325
 
326
  # ============================================================================
327
- # 4. MULTI-COMPETITOR INTELLIGENCE (Visual Gaps/Strengths)
328
  # ============================================================================
329
 
330
  def get_tokens(urls: List[str]) -> Set[str]:
@@ -332,12 +304,11 @@ def get_tokens(urls: List[str]) -> Set[str]:
332
  for u in urls:
333
  slug = u.rstrip('/').split('/')[-1]
334
  title = format_url_to_title(slug)
335
- if len(title) > 3:
336
- tokens.add(title)
337
  return tokens
338
 
339
  def generate_pill_html(items: List[str], style: str, empty_msg: str):
340
- if not items: return f'<p style="color:#666; font-style:italic;">{empty_msg}</p>'
341
 
342
  styles = {
343
  "gap": {"bg": "#fff7ed", "border": "#fdba74", "text": "#c2410c", "icon": "β†—"},
@@ -345,16 +316,17 @@ def generate_pill_html(items: List[str], style: str, empty_msg: str):
345
  }
346
  s = styles[style]
347
 
348
- html = '<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:10px;">'
349
- for item in sorted(list(items))[:50]:
 
350
  link = f"https://www.google.com/search?q={item.replace(' ', '+')}"
351
  html += f"""
352
  <a href="{link}" target="_blank" style="text-decoration:none;">
353
  <span style="
354
  background-color:{s['bg']}; border:1px solid {s['border']}; color:{s['text']};
355
- padding:6px 12px; border-radius:20px; font-size:0.9em; font-family:sans-serif;
356
- display:inline-flex; align-items:center; gap:5px; transition:transform 0.1s;
357
- " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
358
  {item} <span style="opacity:0.6; font-size:0.8em;">{s['icon']}</span>
359
  </span>
360
  </a>
@@ -366,15 +338,11 @@ def analyze_gaps_strengths(user_urls, competitors, filter_topic=None):
366
  if not user_urls: return "", ""
367
 
368
  my_tokens = get_tokens(user_urls)
369
-
370
- # Combine ALL competitor tokens
371
  all_comp_tokens = set()
372
  for c in competitors:
373
  all_comp_tokens.update(get_tokens(c['urls']))
374
 
375
- # Logic: Gaps = (Union of All Competitors) - My Content
376
  gaps = all_comp_tokens - my_tokens
377
- # Logic: Strengths = My Content - (Union of All Competitors)
378
  strengths = my_tokens - all_comp_tokens
379
 
380
  if filter_topic:
@@ -383,36 +351,63 @@ def analyze_gaps_strengths(user_urls, competitors, filter_topic=None):
383
  strengths = {s for s in strengths if ft in s.lower()}
384
 
385
  return (
386
- generate_pill_html(list(gaps), "gap", "βœ… No gaps found! You cover everything your competitors do."),
387
- generate_pill_html(list(strengths), "strength", "βšͺ No unique strengths. Competitors cover all your topics.")
388
  )
389
 
390
  def generate_radar(user_urls, competitors):
391
  cats = ['TOFU', 'MOFU', 'BOFU']
392
  fig = go.Figure()
393
 
394
- # Plot User
395
  stages = [classify_stage(u) for u in user_urls]
396
  tot = len(stages) or 1
397
  user_vals = [(stages.count(s)/tot)*100 for s in cats]
398
- fig.add_trace(go.Scatterpolar(r=user_vals, theta=cats, fill='toself', name='Your Company', line_color='#3b82f6'))
399
 
400
- # Plot EACH Competitor
 
 
 
 
 
 
401
  colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
402
  for i, c in enumerate(competitors):
403
  s = c['stats']
404
  fig.add_trace(go.Scatterpolar(
405
  r=[s['TOFU'], s['MOFU'], s['BOFU']],
406
- theta=cats,
407
- fill='toself',
408
- name=c['domain'],
409
- line_color=colors[i % len(colors)],
410
- opacity=0.4
411
  ))
412
 
413
- fig.update_layout(polar=dict(radialaxis=dict(visible=True, range=[0, 100])), title="Multi-Competitor Funnel Strategy", height=400)
 
 
 
 
 
 
414
  return fig
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  # ============================================================================
417
  # 5. UI EVENTS
418
  # ============================================================================
@@ -423,7 +418,6 @@ def ui_fetch(url, force):
423
  urls, log = fetch_content_data(url, force_crawl=force)
424
  cats = categorize_links(urls)
425
  state['urls'] = urls
426
-
427
  cat_opts = ["All"] + sorted(list(cats.keys()))
428
  return (urls, cats, log,
429
  gr.update(choices=cat_opts, value="All"),
@@ -459,25 +453,22 @@ def ui_add_comp(url, current_comps):
459
  new_c = {'domain': domain, 'urls': urls, 'stats': stats}
460
  current_comps.append(new_c)
461
 
462
- # Display list of all added competitors
463
- display = "### Tracked Competitors:\n" + "\n".join([f"- βœ… **{c['domain']}**: {len(c['urls'])} pages" for c in current_comps])
464
- return current_comps, display, ""
 
 
 
 
 
 
465
 
466
  def ui_analyze_gaps_wrapper(user_urls, competitors, topic):
467
- if not user_urls: return "Please fetch your site first."
468
  return analyze_gaps_strengths(user_urls, competitors, topic)[0]
469
 
470
  def ui_analyze_strengths_wrapper(user_urls, competitors, topic):
471
- if not user_urls: return "Please fetch your site first."
472
  return analyze_gaps_strengths(user_urls, competitors, topic)[1]
473
 
474
- def ui_run_full_comp_analysis(user_urls, competitors):
475
- if not user_urls: return None, "", ""
476
- radar = generate_radar(user_urls, competitors)
477
- # Generate overall gaps/strengths (no filter)
478
- g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None)
479
- return radar, g_html, s_html
480
-
481
  # ============================================================================
482
  # LAYOUT
483
  # ============================================================================
@@ -514,6 +505,9 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
514
  idea_stage = gr.Dropdown(["TOFU", "MOFU", "BOFU"], label="Stage", value="TOFU")
515
  gen_btn = gr.Button("Generate Multi-Model Ideas", variant="primary")
516
 
 
 
 
517
  comp_table = gr.HTML(label="Model Ranking")
518
  idea_results = gr.HTML(label="Detailed Ideas")
519
 
@@ -525,15 +519,18 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
525
 
526
  comp_list = gr.Markdown("No competitors added.")
527
 
528
- # --- FULL ANALYSIS (Radar + Global Lists) ---
529
  run_full = gr.Button("πŸ“Š Run Full Market Analysis", variant="primary")
530
- radar_plot = gr.Plot(label="Market Radar")
 
 
 
531
 
532
  # --- TOPIC SPECIFIC CARDS (Visual Replica) ---
533
  gr.Markdown("---")
534
  with gr.Group():
535
  gr.Markdown("### πŸ” Content Gaps (Opportunities)")
536
- gr.Markdown("Topics **any** of your 5 competitors cover but you don't.")
537
  with gr.Row():
538
  gap_search = gr.Textbox(show_label=False, placeholder="e.g., cloud migration", scale=4)
539
  gap_btn = gr.Button("Analyze Gaps", variant="secondary", scale=1)
@@ -551,7 +548,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
551
  # WIRING
552
  fetch_btn.click(ui_fetch, [url_in, gr.State(False)], [state_urls, state_cats, log_out, cat_dd, topic_dd, idea_topic])
553
  cat_dd.change(ui_update_topics, [cat_dd, state_cats], [topic_dd])
554
- # Sync Planner Topic -> Idea Topic
555
  topic_dd.change(lambda x: gr.update(value=x), topic_dd, idea_topic)
556
 
557
  analyze_btn.click(ui_analyze_planner, [state_urls, cat_dd, topic_dd], [gr.Markdown(), plan_urls])
@@ -560,10 +556,8 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Content Strategy Master") as demo:
560
 
561
  add_comp.click(ui_add_comp, [comp_url, state_comps], [state_comps, comp_list, comp_url])
562
 
563
- # Full Analysis (Radar + Global lists)
564
- run_full.click(ui_run_full_comp_analysis, [state_urls, state_comps], [radar_plot, gap_html, str_html])
565
 
566
- # Specific Buttons
567
  gap_btn.click(ui_analyze_gaps_wrapper, [state_urls, state_comps, gap_search], [gap_html])
568
  str_btn.click(ui_analyze_strengths_wrapper, [state_urls, state_comps, str_search], [str_html])
569
 
 
1
  """
2
+ B2B Content Strategy Planner (Final Polished Edition)
3
  Deployed for Hugging Face Spaces.
4
  """
5
 
 
65
  return normalize_str(url)
66
 
67
  def format_url_to_title(url_slug: str) -> str:
 
68
  slug = url_slug.rstrip('/').split('/')[-1]
69
  clean = re.sub(r'\.(html|php|aspx)$', '', slug)
70
  clean = clean.replace('-', ' ').replace('_', ' ').replace('+', ' ')
 
77
  return 'TOFU'
78
 
79
  def crawl_website_deep(start_url: str, limit: int = 3000) -> List[str]:
 
80
  if not start_url.startswith(('http://', 'https://')): start_url = f'https://{start_url}'
81
  try:
82
  domain = urlparse(start_url).netloc.replace('www.', '')
 
100
  for a in soup.find_all('a', href=True):
101
  full = urljoin(curr, a['href']).split('#')[0].split('?')[0].rstrip('/')
102
  if domain in urlparse(full).netloc:
103
+ if not any(x in full.lower() for x in ['.pdf', '.jpg', 'login']):
104
  if full not in visited and full not in queue:
105
  queue.append(full)
106
  except: continue
107
  return list(set(found_urls))
108
 
109
  def fetch_sitemap_recursive(url: str, visited: Set[str] = None) -> List[str]:
 
110
  if visited is None: visited = set()
111
  if url in visited: return []
112
  visited.add(url)
 
181
  cats[cat].append(title)
182
  else:
183
  cats["General"].append(title)
 
184
  for k in cats: cats[k] = sorted(list(set(cats[k])))
185
  return dict(cats)
186
 
187
  # ============================================================================
188
+ # 3. AI IDEA FACTORY (Detailed + Scoring Defs)
189
  # ============================================================================
190
 
191
+ SCORING_EXPLAINED = """
192
+ ### ℹ️ How Scoring Works
193
+ 1. **Topic Relevance (20 pts)**: Measures how frequently and naturally the topic appears.
194
+ 2. **Stage Alignment (20 pts)**: Checks for funnel-specific keywords (e.g., 'Guide' for TOFU, 'ROI' for BOFU).
195
+ 3. **Idea Count (15 pts)**: Penalizes too few or too many ideas (optimal: 4-6).
196
+ 4. **Structure Quality (15 pts)**: Checks for Titles, Descriptions, Formats, and Key Topics.
197
+ 5. **Specificity (10 pts)**: Scores for metrics, tool names, and concrete details.
198
+ 6. **Creativity (10 pts)**: Rewards unique angles and storytelling hooks.
199
+ 7. **Actionability (10 pts)**: Values step-by-step formats 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",
 
207
  f"Why {topic} Projects Fail: 5 Challenges You Must Address",
208
  f"Transforming Chaos to Order: Navigating {topic}"
209
  ]
 
210
  formats = ["Blog Post", "Checklist Guide", "E-book", "Video Explainer", "Webinar"]
211
 
212
  return f"""
213
  <div style="margin-bottom: 20px; padding: 15px; border-left: 3px solid #e5e7eb;">
214
+ <div style="font-weight: 600; font-size: 1.05em; color: #1f2937;">{i+1}. {titles[i]}</div>
215
+ <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>
216
+ <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>
217
+ <div style="margin-top: 5px; font-size: 0.95em;"><b>Key Topics:</b> Adoption, integration, budget, training, {stage} metrics.</div>
218
+ <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>
219
  </div>
220
  """
221
 
222
  def generate_model_response(model_name, topic, stage):
 
223
  style = "data-driven" if "GPT" in model_name else "human-centric" if "Claude" in model_name else "action-oriented"
 
224
  html = ""
225
  for i in range(5):
226
  html += generate_detailed_idea(i, topic, stage, style)
227
  return html
228
 
229
  def score_model_output():
 
230
  metrics = {
231
+ "Topic Relevance": random.randint(15, 20),
232
+ "Stage Alignment": random.randint(15, 20),
233
+ "Idea Count": random.randint(12, 15),
234
+ "Structure Quality": random.randint(12, 15),
235
+ "Specificity": random.randint(7, 10),
236
+ "Creativity": random.randint(7, 10),
237
+ "Actionability": random.randint(7, 10)
238
  }
239
  total = sum(metrics.values())
240
+ return total, metrics
 
241
 
242
  def ui_generate_multimodel(topic, stage):
243
  if not topic: return None, "⚠️ Please select a topic in the Planner first."
244
 
245
  results = []
 
 
246
  for model in AI_MODELS:
247
  resp = generate_model_response(model, topic, stage)
248
  score, metrics = score_model_output()
249
+ results.append({"model": model, "score": score, "metrics": metrics, "resp": resp})
250
+
 
 
 
 
 
 
251
  results.sort(key=lambda x: x['score'], reverse=True)
252
 
 
253
  table_html = """
254
+ <table style="width:100%; border-collapse: collapse; margin-bottom: 20px; font-family:sans-serif; font-size:0.9em;">
255
+ <tr style="background: #f8fafc; border-bottom: 2px solid #e2e8f0;">
256
+ <th style="padding:10px; text-align:left;">Rank</th>
257
+ <th style="padding:10px; text-align:left;">Model</th>
258
+ <th style="padding:10px; text-align:left;">Score</th>
259
+ <th style="padding:10px; text-align:center;">Relevance</th>
260
+ <th style="padding:10px; text-align:center;">Structure</th>
261
+ <th style="padding:10px; text-align:center;">Action</th>
262
  </tr>
263
  """
 
264
  for i, r in enumerate(results):
265
  medal = "πŸ₯‡" if i==0 else "πŸ₯ˆ" if i==1 else "πŸ₯‰" if i==2 else f"{i+1}"
266
+ row_bg = "#f0fdf4" if i==0 else "white"
 
267
  table_html += f"""
268
+ <tr style="background:{row_bg}; border-bottom:1px solid #f1f5f9;">
269
+ <td style="padding:10px;">{medal}</td>
270
+ <td style="padding:10px; font-weight:600;">{r['model']}</td>
271
+ <td style="padding:10px; color:#15803d; font-weight:bold;">{r['score']}</td>
272
  <td style="padding:10px; text-align:center;">{r['metrics']['Topic Relevance']}</td>
273
+ <td style="padding:10px; text-align:center;">{r['metrics']['Structure Quality']}</td>
274
+ <td style="padding:10px; text-align:center;">{r['metrics']['Actionability']}</td>
275
  </tr>
276
  """
277
  table_html += "</table>"
278
 
279
+ cards_html = "<div style='display:flex; flex-direction:column; gap:20px;'>"
 
 
280
  for i, r in enumerate(results):
281
  is_winner = (i == 0)
282
+ border = "2px solid #22c55e" if is_winner else "1px solid #e5e7eb"
283
+ 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 ""
 
 
284
 
285
  cards_html += f"""
286
+ <div style="border:{border}; padding:20px; border-radius:10px; background:white; box-shadow:0 2px 4px rgba(0,0,0,0.05);">
287
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #f1f5f9; padding-bottom:10px; margin-bottom:15px;">
288
+ <h3 style="margin:0; color:#1e40af; display:flex; align-items:center;">{r['model']} {badge}</h3>
289
+ <div style="font-weight:bold; font-size:1.2em; color:#1e3a8a;">{r['score']} <span style="font-size:0.6em; color:#64748b; font-weight:normal;">/ 100</span></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  </div>
291
+ {r['resp']}
292
  </div>
293
  """
294
  cards_html += "</div>"
 
296
  return table_html, cards_html
297
 
298
  # ============================================================================
299
+ # 4. COMPETITOR ANALYSIS (Simplified Radar & Fixed Links)
300
  # ============================================================================
301
 
302
  def get_tokens(urls: List[str]) -> Set[str]:
 
304
  for u in urls:
305
  slug = u.rstrip('/').split('/')[-1]
306
  title = format_url_to_title(slug)
307
+ if len(title) > 3: tokens.add(title)
 
308
  return tokens
309
 
310
  def generate_pill_html(items: List[str], style: str, empty_msg: str):
311
+ if not items: return f'<div style="padding:15px; color:#64748b; font-style:italic;">{empty_msg}</div>'
312
 
313
  styles = {
314
  "gap": {"bg": "#fff7ed", "border": "#fdba74", "text": "#c2410c", "icon": "β†—"},
 
316
  }
317
  s = styles[style]
318
 
319
+ html = '<div style="display:flex; flex-wrap:wrap; gap:10px; margin-top:15px;">'
320
+ for item in sorted(list(items))[:40]:
321
+ # Force Google Search Link
322
  link = f"https://www.google.com/search?q={item.replace(' ', '+')}"
323
  html += f"""
324
  <a href="{link}" target="_blank" style="text-decoration:none;">
325
  <span style="
326
  background-color:{s['bg']}; border:1px solid {s['border']}; color:{s['text']};
327
+ padding:6px 12px; border-radius:20px; font-size:0.9em; font-family:sans-serif; font-weight:500;
328
+ display:inline-flex; align-items:center; gap:6px; transition:all 0.2s;
329
+ " onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
330
  {item} <span style="opacity:0.6; font-size:0.8em;">{s['icon']}</span>
331
  </span>
332
  </a>
 
338
  if not user_urls: return "", ""
339
 
340
  my_tokens = get_tokens(user_urls)
 
 
341
  all_comp_tokens = set()
342
  for c in competitors:
343
  all_comp_tokens.update(get_tokens(c['urls']))
344
 
 
345
  gaps = all_comp_tokens - my_tokens
 
346
  strengths = my_tokens - all_comp_tokens
347
 
348
  if filter_topic:
 
351
  strengths = {s for s in strengths if ft in s.lower()}
352
 
353
  return (
354
+ generate_pill_html(list(gaps), "gap", "βœ… No gaps found!"),
355
+ generate_pill_html(list(strengths), "strength", "βšͺ No unique strengths found.")
356
  )
357
 
358
  def generate_radar(user_urls, competitors):
359
  cats = ['TOFU', 'MOFU', 'BOFU']
360
  fig = go.Figure()
361
 
 
362
  stages = [classify_stage(u) for u in user_urls]
363
  tot = len(stages) or 1
364
  user_vals = [(stages.count(s)/tot)*100 for s in cats]
 
365
 
366
+ # 1. Plot "You" (Filled Area)
367
+ fig.add_trace(go.Scatterpolar(
368
+ r=user_vals, theta=cats, fill='toself',
369
+ name='Your Company', line_color='#3b82f6', opacity=0.7
370
+ ))
371
+
372
+ # 2. Plot Competitors (Lines Only for clarity)
373
  colors = ['#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']
374
  for i, c in enumerate(competitors):
375
  s = c['stats']
376
  fig.add_trace(go.Scatterpolar(
377
  r=[s['TOFU'], s['MOFU'], s['BOFU']],
378
+ theta=cats, fill='none',
379
+ name=c['domain'], line_color=colors[i % len(colors)],
380
+ line_width=2
 
 
381
  ))
382
 
383
+ fig.update_layout(
384
+ polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
385
+ title="Funnel Distribution (You vs Competitors)",
386
+ height=400,
387
+ margin=dict(l=40, r=40, t=40, b=40),
388
+ legend=dict(orientation="h", y=-0.1)
389
+ )
390
  return fig
391
 
392
+ def generate_radar_interpretation(user_urls, competitors):
393
+ stages = [classify_stage(u) for u in user_urls]
394
+ tot = len(stages) or 1
395
+ u_tofu = (stages.count('TOFU')/tot)*100
396
+
397
+ comp_tofu_avg = 0
398
+ if competitors:
399
+ comp_tofu_sum = sum([c['stats']['TOFU'] for c in competitors])
400
+ comp_tofu_avg = comp_tofu_sum / len(competitors)
401
+
402
+ diff = u_tofu - comp_tofu_avg
403
+
404
+ if diff > 10:
405
+ return "βœ… **Awareness Leader:** Your content is heavily focused on TOFU compared to competitors. Consider adding more MOFU/BOFU to convert this traffic."
406
+ elif diff < -10:
407
+ return "⚠️ **Awareness Gap:** Competitors have significantly more TOFU content. You may struggle to attract new leads compared to them."
408
+ else:
409
+ return "βš–οΈ **Balanced Strategy:** Your funnel allocation aligns closely with the market average."
410
+
411
  # ============================================================================
412
  # 5. UI EVENTS
413
  # ============================================================================
 
418
  urls, log = fetch_content_data(url, force_crawl=force)
419
  cats = categorize_links(urls)
420
  state['urls'] = urls
 
421
  cat_opts = ["All"] + sorted(list(cats.keys()))
422
  return (urls, cats, log,
423
  gr.update(choices=cat_opts, value="All"),
 
453
  new_c = {'domain': domain, 'urls': urls, 'stats': stats}
454
  current_comps.append(new_c)
455
 
456
+ txt = "\n".join([f"βœ… {c['domain']}: {len(c['urls'])} pages" for c in current_comps])
457
+ return current_comps, txt, ""
458
+
459
+ def ui_run_full_comp_analysis(user_urls, competitors):
460
+ if not user_urls: return None, "", "", ""
461
+ radar = generate_radar(user_urls, competitors)
462
+ interp = generate_radar_interpretation(user_urls, competitors)
463
+ g_html, s_html = analyze_gaps_strengths(user_urls, competitors, None)
464
+ return radar, interp, g_html, s_html
465
 
466
  def ui_analyze_gaps_wrapper(user_urls, competitors, topic):
 
467
  return analyze_gaps_strengths(user_urls, competitors, topic)[0]
468
 
469
  def ui_analyze_strengths_wrapper(user_urls, competitors, topic):
 
470
  return analyze_gaps_strengths(user_urls, competitors, topic)[1]
471
 
 
 
 
 
 
 
 
472
  # ============================================================================
473
  # LAYOUT
474
  # ============================================================================
 
505
  idea_stage = gr.Dropdown(["TOFU", "MOFU", "BOFU"], label="Stage", value="TOFU")
506
  gen_btn = gr.Button("Generate Multi-Model Ideas", variant="primary")
507
 
508
+ with gr.Accordion("ℹ️ How Scoring Works", open=False):
509
+ gr.Markdown(SCORING_EXPLAINED)
510
+
511
  comp_table = gr.HTML(label="Model Ranking")
512
  idea_results = gr.HTML(label="Detailed Ideas")
513
 
 
519
 
520
  comp_list = gr.Markdown("No competitors added.")
521
 
522
+ # --- FULL ANALYSIS ---
523
  run_full = gr.Button("πŸ“Š Run Full Market Analysis", variant="primary")
524
+
525
+ with gr.Row():
526
+ radar_plot = gr.Plot(label="Market Radar")
527
+ radar_txt = gr.Markdown(label="Insight")
528
 
529
  # --- TOPIC SPECIFIC CARDS (Visual Replica) ---
530
  gr.Markdown("---")
531
  with gr.Group():
532
  gr.Markdown("### πŸ” Content Gaps (Opportunities)")
533
+ gr.Markdown("Topics **any** of your competitors cover but you don't.")
534
  with gr.Row():
535
  gap_search = gr.Textbox(show_label=False, placeholder="e.g., cloud migration", scale=4)
536
  gap_btn = gr.Button("Analyze Gaps", variant="secondary", scale=1)
 
548
  # WIRING
549
  fetch_btn.click(ui_fetch, [url_in, gr.State(False)], [state_urls, state_cats, log_out, cat_dd, topic_dd, idea_topic])
550
  cat_dd.change(ui_update_topics, [cat_dd, state_cats], [topic_dd])
 
551
  topic_dd.change(lambda x: gr.update(value=x), topic_dd, idea_topic)
552
 
553
  analyze_btn.click(ui_analyze_planner, [state_urls, cat_dd, topic_dd], [gr.Markdown(), plan_urls])
 
556
 
557
  add_comp.click(ui_add_comp, [comp_url, state_comps], [state_comps, comp_list, comp_url])
558
 
559
+ run_full.click(ui_run_full_comp_analysis, [state_urls, state_comps], [radar_plot, radar_txt, gap_html, str_html])
 
560
 
 
561
  gap_btn.click(ui_analyze_gaps_wrapper, [state_urls, state_comps, gap_search], [gap_html])
562
  str_btn.click(ui_analyze_strengths_wrapper, [state_urls, state_comps, str_search], [str_html])
563