Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
B2B Content Strategy Planner (
|
| 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'
|
| 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.
|
| 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:
|
| 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(
|
| 229 |
-
"Stage Alignment": random.randint(
|
| 230 |
-
"
|
| 231 |
-
"Structure": random.randint(
|
| 232 |
-
"
|
| 233 |
-
"
|
| 234 |
-
"
|
| 235 |
}
|
| 236 |
total = sum(metrics.values())
|
| 237 |
-
|
| 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 |
-
|
| 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: #
|
| 263 |
-
<th style="padding:10px;">Rank</th>
|
| 264 |
-
<th style="padding:10px;">
|
| 265 |
-
<th style="padding:10px;">Score</th>
|
| 266 |
-
<th style="padding:10px;">Relevance</th>
|
| 267 |
-
<th style="padding:10px;">
|
| 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 |
-
|
| 275 |
-
weight = "bold" if i==0 else "normal"
|
| 276 |
table_html += f"""
|
| 277 |
-
<tr style="background:{
|
| 278 |
-
<td style="padding:10px;
|
| 279 |
-
<td style="padding:10px;">{r['model']}</td>
|
| 280 |
-
<td style="padding:10px; color:#
|
| 281 |
<td style="padding:10px; text-align:center;">{r['metrics']['Topic Relevance']}</td>
|
| 282 |
-
<td style="padding:10px; text-align:center;">{r['metrics']['
|
| 283 |
-
<td style="padding:10px; text-align:center;">{r['metrics']['
|
| 284 |
</tr>
|
| 285 |
"""
|
| 286 |
table_html += "</table>"
|
| 287 |
|
| 288 |
-
|
| 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 |
-
|
| 294 |
-
|
| 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:{
|
| 300 |
-
<div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #
|
| 301 |
-
<
|
| 302 |
-
|
| 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.
|
| 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'<
|
| 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:
|
| 349 |
-
for item in sorted(list(items))[:
|
|
|
|
| 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:
|
| 357 |
-
" onmouseover="this.style.transform='
|
| 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!
|
| 387 |
-
generate_pill_html(list(strengths), "strength", "βͺ No unique strengths
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 408 |
-
|
| 409 |
-
line_color=colors[i % len(colors)],
|
| 410 |
-
opacity=0.4
|
| 411 |
))
|
| 412 |
|
| 413 |
-
fig.update_layout(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 463 |
-
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 529 |
run_full = gr.Button("π Run Full Market Analysis", variant="primary")
|
| 530 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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 |
|