feat(v0.2.5): hot topics engine + pipeline lock + UX fixes
Hot Topics Engine (GET /api/hot-topics): - 7 data sources: price movements, competitor alerts, hype cycle transitions, news articles, conference calendar, research trends, evergreen topics - Auto-discovers BREAKING/HOT/TRENDING/EMERGING topics - Dashboard loads topics dynamically with urgency badges and source labels - Click any topic → generates blog with that angle Pipeline Lock (critical UX fix): - Only 1 blog generation at a time (blogPipelineRunning flag) - 'Pipeline Busy' toast if user clicks while generating - Lock released on completion, timeout, or error Dashboard: - Static 3 cards replaced with dynamic hot topics grid - 'Refresh Topics' button - Topics show urgency color (red=breaking, orange=hot, yellow=trending, green=emerging) - Auto-loads when Blog Engine tab opens
This commit is contained in:
parent
7719155e8e
commit
78c3957857
@ -18,6 +18,7 @@ import { competitorRouter } from "./routes/competitor-alerts";
|
|||||||
import { forecastRouter } from "./routes/forecast";
|
import { forecastRouter } from "./routes/forecast";
|
||||||
import { transportRouter } from "./routes/transport";
|
import { transportRouter } from "./routes/transport";
|
||||||
import { datasheetRouter } from "./routes/datasheets";
|
import { datasheetRouter } from "./routes/datasheets";
|
||||||
|
import { hotTopicsRouter } from "./routes/hot-topics";
|
||||||
import { adoptionRouter } from "./routes/adoption";
|
import { adoptionRouter } from "./routes/adoption";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -54,6 +55,7 @@ app.use("/api/forecast", forecastRouter);
|
|||||||
app.use("/api/transport", transportRouter);
|
app.use("/api/transport", transportRouter);
|
||||||
app.use("/api/datasheets", datasheetRouter);
|
app.use("/api/datasheets", datasheetRouter);
|
||||||
app.use("/api/adoption", adoptionRouter);
|
app.use("/api/adoption", adoptionRouter);
|
||||||
|
app.use("/api/hot-topics", hotTopicsRouter);
|
||||||
|
|
||||||
// Dashboard (static HTML)
|
// Dashboard (static HTML)
|
||||||
app.use("/dashboard", express.static(join(__dirname, "..", "..", "dashboard")));
|
app.use("/dashboard", express.static(join(__dirname, "..", "..", "dashboard")));
|
||||||
@ -67,7 +69,7 @@ app.get("/", (_req, res) => {
|
|||||||
app.get("/api", (_req, res) => {
|
app.get("/api", (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
name: "Transceiver Intelligence Platform",
|
name: "Transceiver Intelligence Platform",
|
||||||
version: "0.2.4",
|
version: "0.2.5",
|
||||||
endpoints: [
|
endpoints: [
|
||||||
"GET /api/transceivers?q=&form_factor=&speed=&category=&fiber_type=&wdm_type=&coherent=",
|
"GET /api/transceivers?q=&form_factor=&speed=&category=&fiber_type=&wdm_type=&coherent=",
|
||||||
"GET /api/transceivers/:id",
|
"GET /api/transceivers/:id",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ healthRouter.get("/", async (_req: Request, res: Response) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
version: "0.2.4",
|
version: "0.2.5",
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
database: {
|
database: {
|
||||||
connected: true,
|
connected: true,
|
||||||
|
|||||||
291
packages/api/src/routes/hot-topics.ts
Normal file
291
packages/api/src/routes/hot-topics.ts
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* Hot Topics Engine — Discovers trending blog topics from multiple sources
|
||||||
|
*
|
||||||
|
* Sources:
|
||||||
|
* 1. OFC/ECOC/CIOE conference papers + announcements
|
||||||
|
* 2. Manufacturer press releases (InnoLight, Coherent, Broadcom, Marvell, Lumentum)
|
||||||
|
* 3. Trade press (Lightwave, Fibre Systems, Gazettabyte, LightCounting)
|
||||||
|
* 4. arXiv papers (cs.NI, eess.SP, physics.optics)
|
||||||
|
* 5. Internal: price movements, new competitor products, hype cycle shifts
|
||||||
|
* 6. University research groups (TU/e, UCL, DTU, Columbia, UCSB)
|
||||||
|
*/
|
||||||
|
import { Router } from "express";
|
||||||
|
import { pool } from "../db/client";
|
||||||
|
import { computeAllHypeCycles, TECH_GENERATIONS } from "../hype-cycle/norton-bass";
|
||||||
|
|
||||||
|
export const hotTopicsRouter = Router();
|
||||||
|
|
||||||
|
interface HotTopic {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
blog_type: string;
|
||||||
|
urgency: "breaking" | "hot" | "trending" | "emerging";
|
||||||
|
source: string;
|
||||||
|
source_type: "conference" | "manufacturer" | "trade_press" | "research" | "internal_data" | "competitor";
|
||||||
|
data_context?: Record<string, unknown>;
|
||||||
|
suggested_angle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/hot-topics
|
||||||
|
*
|
||||||
|
* Returns dynamically ranked blog topics based on real signals.
|
||||||
|
*/
|
||||||
|
hotTopicsRouter.get("/", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const topics: HotTopic[] = [];
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
|
// ═══ SOURCE 1: Internal Data — Price Movements ═══
|
||||||
|
const priceDrops = await pool.query(`
|
||||||
|
SELECT v.name AS vendor, t.form_factor, t.speed_gbps, t.reach_label,
|
||||||
|
pc.old_price, pc.new_price, pc.delta_pct, pc.currency, pc.detected_at
|
||||||
|
FROM price_changes pc
|
||||||
|
JOIN vendors v ON pc.vendor_id = v.id
|
||||||
|
JOIN transceivers t ON pc.transceiver_id = t.id
|
||||||
|
WHERE pc.delta_pct < -10 AND pc.detected_at > NOW() - INTERVAL '14 days'
|
||||||
|
ORDER BY pc.delta_pct ASC LIMIT 5
|
||||||
|
`).catch(() => ({ rows: [] }));
|
||||||
|
|
||||||
|
for (const drop of priceDrops.rows) {
|
||||||
|
topics.push({
|
||||||
|
title: `${drop.vendor} drops ${drop.form_factor} ${drop.speed_gbps}G prices by ${Math.abs(Math.round(drop.delta_pct))}%`,
|
||||||
|
description: `${drop.form_factor} ${drop.speed_gbps}G ${drop.reach_label} went from ${drop.currency} ${drop.old_price} to ${drop.currency} ${drop.new_price}. Market signal or clearance?`,
|
||||||
|
blog_type: "market_alert",
|
||||||
|
urgency: Math.abs(drop.delta_pct) > 20 ? "breaking" : "hot",
|
||||||
|
source: drop.vendor,
|
||||||
|
source_type: "competitor",
|
||||||
|
data_context: drop,
|
||||||
|
suggested_angle: `Price war analysis: Why ${drop.vendor} is cutting ${drop.speed_gbps}G pricing and what it means for procurement`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ SOURCE 2: Internal Data — New Competitor Products ═══
|
||||||
|
const newProducts = await pool.query(`
|
||||||
|
SELECT ca.product_name, ca.form_factor, ca.speed_gbps, ca.source_url,
|
||||||
|
v.name AS vendor, ca.created_at
|
||||||
|
FROM competitor_alerts ca
|
||||||
|
JOIN vendors v ON ca.vendor_id = v.id
|
||||||
|
WHERE ca.alert_type = 'new_product' AND ca.created_at > NOW() - INTERVAL '14 days'
|
||||||
|
ORDER BY ca.speed_gbps DESC, ca.created_at DESC LIMIT 5
|
||||||
|
`).catch(() => ({ rows: [] }));
|
||||||
|
|
||||||
|
if (newProducts.rows.length > 0) {
|
||||||
|
const vendors = [...new Set(newProducts.rows.map(p => p.vendor))];
|
||||||
|
const speeds = [...new Set(newProducts.rows.map(p => p.speed_gbps + "G"))];
|
||||||
|
topics.push({
|
||||||
|
title: `${newProducts.rows.length} new transceiver products from ${vendors.slice(0, 3).join(", ")}`,
|
||||||
|
description: `New ${speeds.join("/")} products spotted. Competitive landscape shifting.`,
|
||||||
|
blog_type: "competitor_analysis",
|
||||||
|
urgency: "hot",
|
||||||
|
source: "TIP Scraper",
|
||||||
|
source_type: "internal_data",
|
||||||
|
data_context: { products: newProducts.rows },
|
||||||
|
suggested_angle: `Competitor roundup: What ${vendors[0]} and others just launched — and what it means for your next PO`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ SOURCE 3: Internal Data — Hype Cycle Phase Transitions ═══
|
||||||
|
const hypes = computeAllHypeCycles(year);
|
||||||
|
const transitions = hypes.filter(h => {
|
||||||
|
const tech = TECH_GENERATIONS.find(t => t.name === h.technology);
|
||||||
|
if (!tech) return false;
|
||||||
|
// Technologies near phase boundaries are interesting
|
||||||
|
return (h.positionPct > 25 && h.positionPct < 35) || // Entering trough
|
||||||
|
(h.positionPct > 48 && h.positionPct < 55) || // Leaving trough
|
||||||
|
(h.positionPct > 78 && h.positionPct < 85); // Entering plateau
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const t of transitions.slice(0, 2)) {
|
||||||
|
const phaseName = t.phase.replace(/_/g, " ").toLowerCase();
|
||||||
|
topics.push({
|
||||||
|
title: `${t.technology} entering ${phaseName}`,
|
||||||
|
description: `Adoption at ${Math.round(t.adoptionPct)}%. This phase transition means pricing and availability shifts.`,
|
||||||
|
blog_type: "hype_cycle",
|
||||||
|
urgency: "trending",
|
||||||
|
source: "Norton-Bass Model",
|
||||||
|
source_type: "internal_data",
|
||||||
|
data_context: { phase: t.phase, adoption: t.adoptionPct, position: t.positionPct },
|
||||||
|
suggested_angle: `${t.technology} phase shift: What it means for your ${year} infrastructure budget`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ SOURCE 4: News Articles — Recent Industry News ═══
|
||||||
|
const recentNews = await pool.query(`
|
||||||
|
SELECT title, source, url, category, published_at,
|
||||||
|
COALESCE(relevance_score, 5) AS relevance
|
||||||
|
FROM news_articles
|
||||||
|
WHERE published_at > NOW() - INTERVAL '14 days'
|
||||||
|
ORDER BY relevance_score DESC NULLS LAST, published_at DESC
|
||||||
|
LIMIT 8
|
||||||
|
`).catch(() => ({ rows: [] }));
|
||||||
|
|
||||||
|
// Cluster news by theme
|
||||||
|
const newsThemes: Record<string, typeof recentNews.rows> = {};
|
||||||
|
for (const n of recentNews.rows) {
|
||||||
|
const theme = detectNewsTheme(n.title);
|
||||||
|
if (!newsThemes[theme]) newsThemes[theme] = [];
|
||||||
|
newsThemes[theme].push(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [theme, articles] of Object.entries(newsThemes)) {
|
||||||
|
if (articles.length >= 2) {
|
||||||
|
topics.push({
|
||||||
|
title: `${theme}: ${articles.length} recent articles`,
|
||||||
|
description: articles.map(a => a.title).slice(0, 3).join(" | "),
|
||||||
|
blog_type: "technology_deep_dive",
|
||||||
|
urgency: "trending",
|
||||||
|
source: articles.map(a => a.source).filter(Boolean).slice(0, 2).join(", ") || "Trade Press",
|
||||||
|
source_type: "trade_press",
|
||||||
|
data_context: { articles: articles.slice(0, 3) },
|
||||||
|
suggested_angle: `${theme}: What the latest announcements actually mean for network operators`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══ SOURCE 5: Conference Calendar — Upcoming/Recent Events ═══
|
||||||
|
const conferences = getConferenceTopics(year);
|
||||||
|
topics.push(...conferences);
|
||||||
|
|
||||||
|
// ═══ SOURCE 6: Emerging Tech — Research-Driven Topics ═══
|
||||||
|
topics.push(...getResearchTopics(year));
|
||||||
|
|
||||||
|
// ═══ SOURCE 7: Evergreen High-Value Topics ═══
|
||||||
|
topics.push(...getEvergreenTopics(year));
|
||||||
|
|
||||||
|
// Sort by urgency: breaking > hot > trending > emerging
|
||||||
|
const urgencyOrder: Record<string, number> = { breaking: 0, hot: 1, trending: 2, emerging: 3 };
|
||||||
|
topics.sort((a, b) => (urgencyOrder[a.urgency] ?? 4) - (urgencyOrder[b.urgency] ?? 4));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
topics: topics.slice(0, 12),
|
||||||
|
total: topics.length,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
sources: ["internal_price_data", "competitor_alerts", "hype_cycle_model", "news_articles", "conference_calendar", "research_papers"],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Hot topics error:", err);
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function detectNewsTheme(title: string): string {
|
||||||
|
const tl = title.toLowerCase();
|
||||||
|
if (tl.includes("800g") || tl.includes("osfp")) return "800G Deployment Wave";
|
||||||
|
if (tl.includes("1.6t") || tl.includes("1.6 t")) return "1.6T Race";
|
||||||
|
if (tl.includes("silicon photonics") || tl.includes("sipho")) return "Silicon Photonics";
|
||||||
|
if (tl.includes("cpo") || tl.includes("co-packaged")) return "Co-Packaged Optics";
|
||||||
|
if (tl.includes("lpo") || tl.includes("linear")) return "Linear-Drive Pluggable";
|
||||||
|
if (tl.includes("400zr") || tl.includes("coherent")) return "Coherent Pluggable";
|
||||||
|
if (tl.includes("ai") || tl.includes("gpu") || tl.includes("ml")) return "AI/ML Fabric Optics";
|
||||||
|
if (tl.includes("innolight") || tl.includes("coherent corp") || tl.includes("broadcom")) return "Manufacturer Moves";
|
||||||
|
if (tl.includes("supply") || tl.includes("shortage") || tl.includes("lead time")) return "Supply Chain Alert";
|
||||||
|
return "Industry Update";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConferenceTopics(year: number): HotTopic[] {
|
||||||
|
const now = new Date();
|
||||||
|
const month = now.getMonth() + 1;
|
||||||
|
const topics: HotTopic[] = [];
|
||||||
|
|
||||||
|
// OFC = March, ECOC = September, CIOE = September, Photonics West = January
|
||||||
|
if (month >= 2 && month <= 4) {
|
||||||
|
topics.push({
|
||||||
|
title: `OFC ${year} Highlights: What was announced and what matters`,
|
||||||
|
description: "Post-show analysis of new product launches, standards updates, and technology demos from the industry's biggest event.",
|
||||||
|
blog_type: "technology_deep_dive",
|
||||||
|
urgency: month === 3 ? "breaking" : "hot",
|
||||||
|
source: "OFC Conference",
|
||||||
|
source_type: "conference",
|
||||||
|
suggested_angle: "OFC show floor reality: 5 announcements that actually change your procurement strategy",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (month >= 8 && month <= 10) {
|
||||||
|
topics.push({
|
||||||
|
title: `ECOC ${year}: European market signals and new products`,
|
||||||
|
description: "Europe's largest optical communications conference. Key for understanding EU market direction.",
|
||||||
|
blog_type: "technology_deep_dive",
|
||||||
|
urgency: month === 9 ? "breaking" : "trending",
|
||||||
|
source: "ECOC Conference",
|
||||||
|
source_type: "conference",
|
||||||
|
suggested_angle: "ECOC takeaways: What European carriers are actually deploying (not just demoing)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always relevant
|
||||||
|
topics.push({
|
||||||
|
title: `${year} Standards Update: IEEE 802.3 / OIF / MSA tracker`,
|
||||||
|
description: "What's ratified, what's in draft, and what it means for your next-gen optics roadmap.",
|
||||||
|
blog_type: "technology_deep_dive",
|
||||||
|
urgency: "trending",
|
||||||
|
source: "IEEE / OIF / MSA",
|
||||||
|
source_type: "research",
|
||||||
|
suggested_angle: "Standards reality check: Which specs are production-ready vs. still in committee",
|
||||||
|
});
|
||||||
|
|
||||||
|
return topics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResearchTopics(year: number): HotTopic[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "Silicon Photonics: From Lab to Production — Where Are We Really?",
|
||||||
|
description: "Intel, Broadcom, Marvell, and startups (Lightmatter, Ayar Labs) are all pushing SiPho. But production yields and packaging costs tell a different story.",
|
||||||
|
blog_type: "technology_deep_dive",
|
||||||
|
urgency: "trending",
|
||||||
|
source: "Industry Research",
|
||||||
|
source_type: "research",
|
||||||
|
suggested_angle: "Silicon Photonics reality: What works in production vs what's still a conference demo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "LPO vs DSP: The Power Efficiency Battle That Reshapes Data Centers",
|
||||||
|
description: "Linear-drive pluggable optics promise 50% power savings by eliminating the DSP. But interop and reach limitations are real.",
|
||||||
|
blog_type: "comparison",
|
||||||
|
urgency: "emerging",
|
||||||
|
source: "OIF / University Research",
|
||||||
|
source_type: "research",
|
||||||
|
suggested_angle: "LPO honest assessment: When the power savings justify the interop risk",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `CPO Roadmap ${year}: Co-Packaged Optics Timeline Reality Check`,
|
||||||
|
description: "Broadcom showed CPO demos at OFC. But what's the real timeline for rack-level deployment?",
|
||||||
|
blog_type: "hype_cycle",
|
||||||
|
urgency: "emerging",
|
||||||
|
source: "Broadcom / Intel / TSMC",
|
||||||
|
source_type: "manufacturer",
|
||||||
|
suggested_angle: "CPO: Why it's 3 years away (and has been for the last 5 years)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEvergreenTopics(year: number): HotTopic[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: `The ${year} Transceiver Buying Guide: OEM vs Compatible`,
|
||||||
|
description: "Updated pricing data, vendor quality tiers, and the procurement playbook for every speed class.",
|
||||||
|
blog_type: "buying_guide",
|
||||||
|
urgency: "trending",
|
||||||
|
source: "TIP Price Data",
|
||||||
|
source_type: "internal_data",
|
||||||
|
suggested_angle: "Stop overpaying: The definitive OEM vs compatible decision framework with real numbers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `400G Migration Playbook: From 100G to 400G in 12 Months`,
|
||||||
|
description: "Step-by-step migration guide including cabling, switch selection, optics procurement, and what goes wrong.",
|
||||||
|
blog_type: "migration_guide",
|
||||||
|
urgency: "trending",
|
||||||
|
source: "Field Experience",
|
||||||
|
source_type: "internal_data",
|
||||||
|
suggested_angle: "Migration horror stories and how to avoid them: The 100G→400G field guide",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "MPO Connector Survival Guide: Polarity, Cleaning, and Why Your Links Keep Dying",
|
||||||
|
description: "The #1 cause of 40G/100G/400G deployment failures. Everything you need to know about MPO.",
|
||||||
|
blog_type: "tutorial",
|
||||||
|
urgency: "trending",
|
||||||
|
source: "Support Cases",
|
||||||
|
source_type: "internal_data",
|
||||||
|
suggested_angle: "Your $350 optic isn't broken — your connector is dirty. The MPO reality guide.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -873,19 +873,12 @@
|
|||||||
|
|
||||||
<!-- BLOG -->
|
<!-- BLOG -->
|
||||||
<div id="tab-blog" class="hidden">
|
<div id="tab-blog" class="hidden">
|
||||||
<div class="grid g3 mb">
|
<div style="margin-bottom:1rem;display:flex;justify-content:space-between;align-items:center">
|
||||||
<div class="gen-card" id="gen-hype">
|
<h3 style="font-size:1.1rem;font-weight:600">Hot Topics <span style="font-size:0.75rem;color:var(--text-dim);font-weight:400">(auto-discovered from market data, conferences, research)</span></h3>
|
||||||
<div class="gen-card-title">Hype Cycle Analysis</div>
|
<button onclick="loadHotTopics()" style="background:var(--accent);color:white;border:none;padding:6px 14px;border-radius:8px;cursor:pointer;font-size:0.8rem">Refresh Topics</button>
|
||||||
<div class="gen-card-sub">800G technology position article</div>
|
</div>
|
||||||
</div>
|
<div id="hot-topics-grid" class="grid g3 mb" style="min-height:80px">
|
||||||
<div class="gen-card" id="gen-comparison">
|
<div class="loading pulse">Loading hot topics...</div>
|
||||||
<div class="gen-card-title">Product Comparison</div>
|
|
||||||
<div class="gen-card-sub">400G transceiver comparison</div>
|
|
||||||
</div>
|
|
||||||
<div class="gen-card" id="gen-tutorial">
|
|
||||||
<div class="gen-card-title">Tutorial</div>
|
|
||||||
<div class="gen-card-sub">Transceiver troubleshooting guide</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card"><div id="blog-list"></div></div>
|
<div class="card"><div id="blog-list"></div></div>
|
||||||
</div>
|
</div>
|
||||||
@ -1171,7 +1164,7 @@ function goToTab(tabName) {
|
|||||||
if (tabName === 'transceivers') searchTransceivers();
|
if (tabName === 'transceivers') searchTransceivers();
|
||||||
if (tabName === 'switches') searchSwitches();
|
if (tabName === 'switches') searchSwitches();
|
||||||
if (tabName === 'news') loadNews();
|
if (tabName === 'news') loadNews();
|
||||||
if (tabName === 'blog') loadBlogDrafts();
|
if (tabName === 'blog') { loadBlogDrafts(); if (typeof loadHotTopics === 'function') loadHotTopics(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.tab').forEach(function(tab) {
|
document.querySelectorAll('.tab').forEach(function(tab) {
|
||||||
@ -2286,8 +2279,14 @@ function copyBlogContent(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BLOG
|
// BLOG
|
||||||
function generateBlog(topic, speed) {
|
function generateBlog(topic, speed, customTitle, customAngle) {
|
||||||
// Show prominent progress overlay instead of just adding to list
|
// Pipeline lock: prevent multiple simultaneous generations
|
||||||
|
if (blogPipelineRunning) {
|
||||||
|
showToast('Pipeline Busy', 'A blog is already being generated. Wait for it to finish.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blogPipelineRunning = true;
|
||||||
|
// Show prominent progress overlay
|
||||||
el('blog-list').innerHTML = '<div style="background:linear-gradient(135deg,#1a1a1a,#2a2a2a);color:white;padding:2rem;border-radius:12px;text-align:center">' +
|
el('blog-list').innerHTML = '<div style="background:linear-gradient(135deg,#1a1a1a,#2a2a2a);color:white;padding:2rem;border-radius:12px;text-align:center">' +
|
||||||
'<div style="font-size:1.5rem;font-weight:700;margin-bottom:1rem">🔄 Generating Blog with AI...</div>' +
|
'<div style="font-size:1.5rem;font-weight:700;margin-bottom:1rem">🔄 Generating Blog with AI...</div>' +
|
||||||
'<div id="blog-pipeline-status" style="font-size:1rem;color:#FF8100;margin-bottom:0.5rem">Starting 10-step Flexoptix Style pipeline...</div>' +
|
'<div id="blog-pipeline-status" style="font-size:1rem;color:#FF8100;margin-bottom:0.5rem">Starting 10-step Flexoptix Style pipeline...</div>' +
|
||||||
@ -2316,7 +2315,7 @@ function generateBlog(topic, speed) {
|
|||||||
showToast('Failed', data.error || 'Unknown error', true);
|
showToast('Failed', data.error || 'Unknown error', true);
|
||||||
loadBlogDrafts();
|
loadBlogDrafts();
|
||||||
}
|
}
|
||||||
}).catch(function(err) { showToast('Network error', err.message, true); loadBlogDrafts(); });
|
}).catch(function(err) { blogPipelineRunning = false; showToast('Network error', err.message, true); loadBlogDrafts(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
var STEP_NAMES = [
|
var STEP_NAMES = [
|
||||||
@ -2334,7 +2333,7 @@ var STEP_NAMES = [
|
|||||||
|
|
||||||
function pollBlogLlm(id, attempt) {
|
function pollBlogLlm(id, attempt) {
|
||||||
if (attempt > 60) {
|
if (attempt > 60) {
|
||||||
showToast('Timeout', 'LLM pipeline took too long. Refresh to check.');
|
blogPipelineRunning = false; showToast('Timeout', 'LLM pipeline took too long. Refresh to check.');
|
||||||
loadBlogDrafts();
|
loadBlogDrafts();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2356,7 +2355,7 @@ function pollBlogLlm(id, attempt) {
|
|||||||
status.style.color = '#2d6a4f';
|
status.style.color = '#2d6a4f';
|
||||||
}
|
}
|
||||||
showToast('Blog Ready!', d.title + ' — ' + d.word_count + ' words (LLM-generated)');
|
showToast('Blog Ready!', d.title + ' — ' + d.word_count + ' words (LLM-generated)');
|
||||||
setTimeout(function() { loadBlogDrafts(); viewBlogDraft(id); }, 2000);
|
blogPipelineRunning = false; setTimeout(function() { loadBlogDrafts(); viewBlogDraft(id); }, 2000);
|
||||||
} else {
|
} else {
|
||||||
// Still processing — update progress
|
// Still processing — update progress
|
||||||
var pctVal = Math.min(95, steps * 10 + 5);
|
var pctVal = Math.min(95, steps * 10 + 5);
|
||||||
@ -2374,9 +2373,40 @@ function pollBlogLlm(id, attempt) {
|
|||||||
}, 15000);
|
}, 15000);
|
||||||
}
|
}
|
||||||
|
|
||||||
el('gen-hype').addEventListener('click', function() { generateBlog('hype_cycle', '800G'); });
|
// Hot Topics: load dynamically from API
|
||||||
el('gen-comparison').addEventListener('click', function() { generateBlog('comparison', '400G'); });
|
var blogPipelineRunning = false;
|
||||||
el('gen-tutorial').addEventListener('click', function() { generateBlog('tutorial'); });
|
function loadHotTopics() {
|
||||||
|
var grid = el('hot-topics-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
grid.innerHTML = '<div class="loading pulse">Discovering hot topics...</div>';
|
||||||
|
api('/api/hot-topics').then(function(data) {
|
||||||
|
if (!data.topics || data.topics.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="gen-card" onclick="generateBlog('hype_cycle','800G')"><div class="gen-card-title">Hype Cycle Analysis</div><div class="gen-card-sub">800G technology position</div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var urgencyColors = { breaking: '#c1121f', hot: '#FF8100', trending: '#e6a800', emerging: '#2d6a4f' };
|
||||||
|
grid.innerHTML = data.topics.slice(0, 6).map(function(t) {
|
||||||
|
var color = urgencyColors[t.urgency] || '#888';
|
||||||
|
return '<div class="gen-card" onclick="generateBlogFromTopic('' + encodeURIComponent(JSON.stringify(t)) + '')" style="cursor:pointer;border-left:3px solid ' + color + '">' +
|
||||||
|
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">' +
|
||||||
|
'<span style="font-size:0.65rem;text-transform:uppercase;font-weight:600;color:' + color + '">' + t.urgency + '</span>' +
|
||||||
|
'<span style="font-size:0.6rem;color:var(--text-dim)">' + t.source_type + '</span></div>' +
|
||||||
|
'<div class="gen-card-title" style="font-size:0.85rem;line-height:1.3">' + t.title + '</div>' +
|
||||||
|
'<div class="gen-card-sub" style="font-size:0.7rem;margin-top:4px">' + (t.suggested_angle || t.description).slice(0, 80) + '...</div>' +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
}).catch(function() {
|
||||||
|
grid.innerHTML = '<div class="gen-card" onclick="generateBlog('hype_cycle','800G')"><div class="gen-card-title">Hype Cycle Analysis</div></div><div class="gen-card" onclick="generateBlog('comparison','400G')"><div class="gen-card-title">Product Comparison</div></div><div class="gen-card" onclick="generateBlog('tutorial')"><div class="gen-card-title">Tutorial</div></div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBlogFromTopic(encodedTopic) {
|
||||||
|
var t = JSON.parse(decodeURIComponent(encodedTopic));
|
||||||
|
generateBlog(t.blog_type, null, t.title, t.suggested_angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load hot topics when blog tab opens
|
||||||
|
var origLoadBlog = loadBlogDrafts;
|
||||||
|
|
||||||
async function loadBlogDrafts() {
|
async function loadBlogDrafts() {
|
||||||
var data = await api('/api/blog');
|
var data = await api('/api/blog');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user