feat(shorts): AI agent tips category — ideas, script structure, tips voice, category column
This commit is contained in:
@@ -45,3 +45,4 @@ yarn-error.log*
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.env*.local
|
.env*.local
|
||||||
/services/client-pulse-bot/dist/
|
/services/client-pulse-bot/dist/
|
||||||
|
/tmp/checkcat.mjs
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ model YoutubeIdea {
|
|||||||
funnelStage String?
|
funnelStage String?
|
||||||
status String @default("pending")
|
status String @default("pending")
|
||||||
rejectedReason String?
|
rejectedReason String?
|
||||||
|
category String? @default("brands")
|
||||||
sourceUrl String?
|
sourceUrl String?
|
||||||
sourceSummary String?
|
sourceSummary String?
|
||||||
competitionScore String? // "gap" | "moderate" | "crowded"
|
competitionScore String? // "gap" | "moderate" | "crowded"
|
||||||
@@ -172,6 +173,8 @@ model YoutubeScript {
|
|||||||
// Source content (tweet/thread/news) rendered as visual cards
|
// Source content (tweet/thread/news) rendered as visual cards
|
||||||
sourceUrl String?
|
sourceUrl String?
|
||||||
sourceData Json?
|
sourceData Json?
|
||||||
|
// Content category (brands | ai-agent-tips | ...)
|
||||||
|
category String? @default("brands")
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const EDGE_TTS_BIN =
|
|||||||
? "/home/code/.hermes/hermes-agent/venv/bin/edge-tts"
|
? "/home/code/.hermes/hermes-agent/venv/bin/edge-tts"
|
||||||
: "edge-tts");
|
: "edge-tts");
|
||||||
const VOICE = process.env.TTS_VOICE || "en-US-ChristopherNeural";
|
const VOICE = process.env.TTS_VOICE || "en-US-ChristopherNeural";
|
||||||
|
const VOICE_TIPS = process.env.TTS_VOICE_TIPS || "en-US-GuyNeural"; // tech/explainer
|
||||||
const CHROME = process.env.CHROME_BIN || "/usr/bin/google-chrome-stable";
|
const CHROME = process.env.CHROME_BIN || "/usr/bin/google-chrome-stable";
|
||||||
const MAX_TOTAL_SEC = 60;
|
const MAX_TOTAL_SEC = 60;
|
||||||
const FPS = 30;
|
const FPS = 30;
|
||||||
@@ -111,12 +112,12 @@ function sh(cmd, args, opts = {}) {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tts(text, outBase) {
|
function tts(text, outBase, voice = VOICE) {
|
||||||
const out = `${outBase}.mp3`;
|
const out = `${outBase}.mp3`;
|
||||||
const srt = `${outBase}.sentences.json`;
|
const srt = `${outBase}.sentences.json`;
|
||||||
sh(
|
sh(
|
||||||
TTS_PYTHON,
|
TTS_PYTHON,
|
||||||
[TTS_WORDS_PY, text, VOICE, out, srt],
|
[TTS_WORDS_PY, text, voice, out, srt],
|
||||||
{ timeout: 60_000, env: { ...process.env } }
|
{ timeout: 60_000, env: { ...process.env } }
|
||||||
);
|
);
|
||||||
let sentences = [];
|
let sentences = [];
|
||||||
@@ -166,18 +167,29 @@ function extractStatData(text) {
|
|||||||
|
|
||||||
/** Build segment text list from a YoutubeScript's structured fields. */
|
/** Build segment text list from a YoutubeScript's structured fields. */
|
||||||
function buildSegments(script) {
|
function buildSegments(script) {
|
||||||
|
const isTips = script.category === "ai-agent-tips";
|
||||||
const segs = [];
|
const segs = [];
|
||||||
const push = (text, label, highlight) => {
|
const push = (text, label, highlight) => {
|
||||||
const clean = (text || "").trim();
|
const clean = (text || "").trim();
|
||||||
const chunk = clean.length > 180 ? clean.slice(0, 180) + "…" : clean;
|
const chunk = clean.length > 180 ? clean.slice(0, 180) + "…" : clean;
|
||||||
if (chunk) segs.push({ text: chunk, label, highlight });
|
if (chunk) segs.push({ text: chunk, label, highlight });
|
||||||
};
|
};
|
||||||
push(script.hook, "HOOK", script.hook);
|
// For AI-agent tips: labels = MISTAKE / THE FIX instead of THE STORY / ...
|
||||||
push(script.onScreenText, "ON SCREEN", script.onScreenText);
|
if (isTips) {
|
||||||
push(script.storySetup, "THE STORY", null);
|
push(script.hook, "HOOK", script.hook);
|
||||||
push(script.conflict, "THE CONFLICT", null);
|
push(script.onScreenText, "ON SCREEN", script.onScreenText);
|
||||||
push(script.insight, "THE INSIGHT", null);
|
push(script.storySetup, "THE MISTAKE", null);
|
||||||
push(script.cta, "CTA", null);
|
push(script.conflict, "WHY IT FAILS", null);
|
||||||
|
push(script.insight, "THE FIX", null);
|
||||||
|
push(script.cta, "CTA", null);
|
||||||
|
} else {
|
||||||
|
push(script.hook, "HOOK", script.hook);
|
||||||
|
push(script.onScreenText, "ON SCREEN", script.onScreenText);
|
||||||
|
push(script.storySetup, "THE STORY", null);
|
||||||
|
push(script.conflict, "THE CONFLICT", null);
|
||||||
|
push(script.insight, "THE INSIGHT", null);
|
||||||
|
push(script.cta, "CTA", null);
|
||||||
|
}
|
||||||
if (segs.length === 0) push(script.fullScript || script.title, script.title || "SHORTS", null);
|
if (segs.length === 0) push(script.fullScript || script.title, script.title || "SHORTS", null);
|
||||||
return segs;
|
return segs;
|
||||||
}
|
}
|
||||||
@@ -194,7 +206,7 @@ async function renderScript(script) {
|
|||||||
const voiced = [];
|
const voiced = [];
|
||||||
for (let i = 0; i < baseSegs.length; i++) {
|
for (let i = 0; i < baseSegs.length; i++) {
|
||||||
const s = baseSegs[i];
|
const s = baseSegs[i];
|
||||||
const t = tts(s.text, path.join(work, `seg${i}`));
|
const t = tts(s.text, path.join(work, `seg${i}`), script.category === "ai-agent-tips" ? VOICE_TIPS : VOICE);
|
||||||
voiced.push({ ...s, ...t });
|
voiced.push({ ...s, ...t });
|
||||||
log(`[${id}] seg${i}: ${t.durationSec.toFixed(2)}s "${s.text.slice(0, 40)}"`);
|
log(`[${id}] seg${i}: ${t.durationSec.toFixed(2)}s "${s.text.slice(0, 40)}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ function fetchArticle(url: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
const { topic, sourceUrl, sourceSummary } = await req.json();
|
const { topic, sourceUrl, sourceSummary, category } = await req.json();
|
||||||
if (!topic) return NextResponse.json({ error: "topic required" }, { status: 400 });
|
if (!topic) return NextResponse.json({ error: "topic required" }, { status: 400 });
|
||||||
|
const isTips = category === "ai-agent-tips";
|
||||||
|
|
||||||
const apiKey = process.env.OPENAI_API_KEY || "";
|
const apiKey = process.env.OPENAI_API_KEY || "";
|
||||||
const braveKey = process.env.BRAVE_API_KEY || "";
|
const braveKey = process.env.BRAVE_API_KEY || "";
|
||||||
@@ -171,20 +172,25 @@ ${sourceContext || "No sources available — be EXTRA cautious with claims, hedg
|
|||||||
- Include a "factClaims" array listing every specific factual claim in the script (dates, numbers, events, quotes)
|
- Include a "factClaims" array listing every specific factual claim in the script (dates, numbers, events, quotes)
|
||||||
- Each claim MUST be traceable to the source material above
|
- Each claim MUST be traceable to the source material above
|
||||||
|
|
||||||
## THE USER'S ANGLE
|
## THE USER'S ANGLE${isTips ? `
|
||||||
|
- Builds and ships AI agents (coding agents, MCP, agent loops) — the tips are for OTHER builders
|
||||||
|
- Voice: direct, first-principles, tells you the mistake you are probably making
|
||||||
|
- Every tip must be concrete and actionable — no fluff
|
||||||
|
- The viewer is technical: they write code and wire tools together
|
||||||
|
- Ground each tip in a real failure/lesson: "I ran this in production and learned X"` : `
|
||||||
- Runs a marketing-focused business
|
- Runs a marketing-focused business
|
||||||
- Content: founder branding, marketing, distribution > product, culture > features
|
- Content: founder branding, marketing, distribution > product, culture > features
|
||||||
- Voice: conversational, no fancy words, lowercase energy, real numbers, honest/vulnerable
|
- Voice: conversational, no fancy words, lowercase energy, real numbers, honest/vulnerable`}
|
||||||
${pastScriptsRef}
|
${pastScriptsRef}
|
||||||
${feedbackContext}
|
${feedbackContext}
|
||||||
|
|
||||||
OUTPUT FORMAT — valid JSON only:
|
OUTPUT FORMAT — valid JSON only:
|
||||||
{
|
{
|
||||||
"title": "Brand/Person — Core Lesson (short)",
|
"title": "Brand/Person — Core Lesson (short)",
|
||||||
"hook": "Opening 2-4 lines. Scroll-stopping.",
|
"hook": "Opening 2-4 lines. Scroll-stopping.${isTips ? " Lead with the COMMON MISTAKE or counter-intuitive truth about AI agents." : ""}",
|
||||||
"storySetup": "The backstory context. 3-5 sentences.",
|
"storySetup": "The backstory context. 3-5 sentences.${isTips ? " The situation: why builders get this wrong." : ""}",
|
||||||
"conflict": "The tension pivot or controversial statement. 3-5 sentences.",
|
"conflict": "The tension pivot or controversial statement. 3-5 sentences.${isTips ? " The failure mode / what everyone does that backfires." : ""}",
|
||||||
"insight": "The aha moment payoff lesson for founders. 3-5 sentences.",
|
"insight": "The aha moment payoff lesson for founders. 3-5 sentences.${isTips ? " THE concrete fix/pattern — specific, actionable, names the tool/technique." : ""}",
|
||||||
"cta": "One clear action. One line.",
|
"cta": "One clear action. One line.",
|
||||||
"caption": "One punchy line without spoilers.",
|
"caption": "One punchy line without spoilers.",
|
||||||
"onScreenText": "What appears on screen during the hook",
|
"onScreenText": "What appears on screen during the hook",
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ function braveSearch(query: string, braveKey: string, count = 5): { title: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST(req?: Request) {
|
||||||
|
const body = req ? await req.json().catch(() => ({})) : {};
|
||||||
|
const category: string = body.category || "brands";
|
||||||
|
const isTips = category === "ai-agent-tips";
|
||||||
|
|
||||||
const apiKey = process.env.OPENAI_API_KEY || "";
|
const apiKey = process.env.OPENAI_API_KEY || "";
|
||||||
const braveKey = process.env.BRAVE_API_KEY || "";
|
const braveKey = process.env.BRAVE_API_KEY || "";
|
||||||
if (!apiKey) return NextResponse.json({ error: "No API key" }, { status: 500 });
|
if (!apiKey) return NextResponse.json({ error: "No API key" }, { status: 500 });
|
||||||
@@ -65,10 +69,19 @@ export async function POST() {
|
|||||||
try { coveredTopics = fs.readFileSync(SCRIPTS_FILE, "utf-8").match(/### .+/g)?.join(", ") || ""; } catch { /* ok - file may not exist on Vercel */ }
|
try { coveredTopics = fs.readFileSync(SCRIPTS_FILE, "utf-8").match(/### .+/g)?.join(", ") || ""; } catch { /* ok - file may not exist on Vercel */ }
|
||||||
|
|
||||||
// -- STEP 1: Ask GPT to generate search queries --
|
// -- STEP 1: Ask GPT to generate search queries --
|
||||||
const queryGenResult = callOpenAI(apiKey, {
|
const querySystem = isTips
|
||||||
model: "gpt-4o-mini",
|
? `You generate search queries to find practical, actionable AI AGENT tips and best practices for a viral short-form video creator.
|
||||||
messages: [
|
|
||||||
{ role: "system", content: `You generate search queries to find fascinating REAL stories about brands, businesses, and cultural moments for a viral short-form video creator.
|
The creator makes videos for builders/developers who ship AI agents (coding agents, MCP servers, tool-using LLMs, agent loops).
|
||||||
|
Topics to cover: agent architecture, tool design, context window management, evals/testing, prompt engineering for agents, MCP, sub-agents, cost control, reliability/failure modes, memory, permissions/safety, orchestration patterns.
|
||||||
|
|
||||||
|
Generate 10 diverse search queries for REAL, concrete tips (engineering blogs, docs, HN discussions, practitioner posts). Prefer "best practices", "lessons learned", "common mistakes", "how to", "patterns".
|
||||||
|
|
||||||
|
DO NOT search for ideas already generated: ${existingTitles}
|
||||||
|
${rejectionContext}
|
||||||
|
|
||||||
|
Output JSON: {"queries":["search query 1","search query 2",...]}`
|
||||||
|
: `You generate search queries to find fascinating REAL stories about brands, businesses, and cultural moments for a viral short-form video creator.
|
||||||
|
|
||||||
The creator covers a WIDE mix: mainstream brands (Red Bull, Nike, Barbie, Tesla, Apple, McDonald's, etc.), tech, pop culture, and business strategy. Most videos are about mainstream brands with a marketing/founder lesson.
|
The creator covers a WIDE mix: mainstream brands (Red Bull, Nike, Barbie, Tesla, Apple, McDonald's, etc.), tech, pop culture, and business strategy. Most videos are about mainstream brands with a marketing/founder lesson.
|
||||||
|
|
||||||
@@ -81,8 +94,15 @@ DO NOT search for stories already covered: ${coveredTopics}
|
|||||||
DO NOT search for ideas already generated: ${existingTitles}
|
DO NOT search for ideas already generated: ${existingTitles}
|
||||||
${rejectionContext}
|
${rejectionContext}
|
||||||
|
|
||||||
Output JSON: {"queries":["search query 1","search query 2",...]}` },
|
Output JSON: {"queries":["search query 1","search query 2",...]}`;
|
||||||
{ role: "user", content: "10 diverse search queries for fascinating real stories. Mix mainstream brands, crypto, pop culture." }
|
|
||||||
|
const queryGenResult = callOpenAI(apiKey, {
|
||||||
|
model: "gpt-4o-mini",
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: querySystem },
|
||||||
|
{ role: "user", content: isTips
|
||||||
|
? "10 diverse search queries for real, actionable AI agent tips and lessons."
|
||||||
|
: "10 diverse search queries for fascinating real stories. Mix mainstream brands, crypto, pop culture." }
|
||||||
],
|
],
|
||||||
temperature: 0.95,
|
temperature: 0.95,
|
||||||
response_format: { type: "json_object" }
|
response_format: { type: "json_object" }
|
||||||
@@ -104,10 +124,23 @@ Output JSON: {"queries":["search query 1","search query 2",...]}` },
|
|||||||
).join("\n\n");
|
).join("\n\n");
|
||||||
|
|
||||||
// -- STEP 3: Feed real articles to GPT to create ideas --
|
// -- STEP 3: Feed real articles to GPT to create ideas --
|
||||||
const ideaGenResult = callOpenAI(apiKey, {
|
const ideaSystem = isTips
|
||||||
model: "gpt-4o-mini",
|
? `You create viral short-form video IDEAS about practical AI AGENT tips for builders, based on REAL articles found via web search.
|
||||||
messages: [
|
|
||||||
{ role: "system", content: `You create viral short-form video ideas for the user based on REAL articles and stories found via web search.
|
CRITICAL RULES:
|
||||||
|
- Each idea MUST be one concrete, actionable tip/lesson from the search results below
|
||||||
|
- Each idea MUST include a sourceUrl from the search results
|
||||||
|
- Each idea MUST include a sourceSummary (1-2 sentence summary of the real tip/lesson)
|
||||||
|
- Hook must stop the scroll in 3 seconds — lead with a common MISTAKE or counter-intuitive truth
|
||||||
|
- Examples of strong hooks: "Your AI agent fails because of one missing tool", "Stop giving your agent 40 tools", "Nobody tells you this about MCP servers"
|
||||||
|
- The angle must name a SPECIFIC technique/tool/pattern (MCP, tool schemas, evals, context pruning, sub-agents...) — never vague "use AI better"
|
||||||
|
- Mix funnel stages: 5 TOF (wide builder appeal), 3 MOF (specific workflows), 2 BOF (advanced architecture)
|
||||||
|
- DO NOT repeat these existing ideas: ${existingTitles}
|
||||||
|
- Each hook should have normal punctuation (commas, periods)
|
||||||
|
${rejectionContext}
|
||||||
|
|
||||||
|
Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"specific technique/pattern","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real tip"}]}`
|
||||||
|
: `You create viral short-form video ideas for the user based on REAL articles and stories found via web search.
|
||||||
|
|
||||||
CRITICAL RULES:
|
CRITICAL RULES:
|
||||||
- Each idea MUST be based on a REAL story from the search results below
|
- Each idea MUST be based on a REAL story from the search results below
|
||||||
@@ -124,8 +157,15 @@ CRITICAL RULES:
|
|||||||
- Always name the SPECIFIC brand/person — never be vague
|
- Always name the SPECIFIC brand/person — never be vague
|
||||||
${rejectionContext}
|
${rejectionContext}
|
||||||
|
|
||||||
Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"founder/crypto lesson","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real story"}]}` },
|
Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"founder/crypto lesson","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real story"}]}`;
|
||||||
{ role: "user", content: `Based on these REAL articles found via web search, create 10 video ideas. Each idea MUST reference a real story and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.` }
|
|
||||||
|
const ideaGenResult = callOpenAI(apiKey, {
|
||||||
|
model: "gpt-4o-mini",
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: ideaSystem },
|
||||||
|
{ role: "user", content: isTips
|
||||||
|
? `Based on these REAL articles found via web search, create 10 AI AGENT TIP video ideas. Each idea MUST be one concrete tip and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.`
|
||||||
|
: `Based on these REAL articles found via web search, create 10 video ideas. Each idea MUST reference a real story and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.` }
|
||||||
],
|
],
|
||||||
temperature: 0.85,
|
temperature: 0.85,
|
||||||
response_format: { type: "json_object" }
|
response_format: { type: "json_object" }
|
||||||
@@ -149,6 +189,7 @@ Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scrol
|
|||||||
angle: idea.angle || null,
|
angle: idea.angle || null,
|
||||||
hookType: idea.hookType || null,
|
hookType: idea.hookType || null,
|
||||||
funnelStage: idea.funnelStage || null,
|
funnelStage: idea.funnelStage || null,
|
||||||
|
category,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,15 +22,17 @@ const LongformTab = dynamic(() => import("@/app/longform/page"), { ssr: false, l
|
|||||||
const OutlierFeed = dynamic(() => import("@/components/OutlierFeed"), { ssr: false, loading: () => <TabSkeleton /> });
|
const OutlierFeed = dynamic(() => import("@/components/OutlierFeed"), { ssr: false, loading: () => <TabSkeleton /> });
|
||||||
|
|
||||||
interface Idea {
|
interface Idea {
|
||||||
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
hook: string;
|
hook?: string;
|
||||||
angle: string;
|
angle?: string;
|
||||||
hookType?: string;
|
hookType?: string;
|
||||||
funnelStage?: string;
|
funnelStage?: string;
|
||||||
rejectedReason?: string;
|
category?: string;
|
||||||
status?: "pending" | "rejected" | "approved";
|
|
||||||
sourceUrl?: string;
|
sourceUrl?: string;
|
||||||
sourceSummary?: string;
|
sourceSummary?: string;
|
||||||
|
rejectedReason?: string;
|
||||||
|
status?: "pending" | "rejected" | "approved";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Script {
|
interface Script {
|
||||||
@@ -82,6 +84,7 @@ export default function YouTubePage() {
|
|||||||
const [batchGenerating, setBatchGenerating] = useState(false);
|
const [batchGenerating, setBatchGenerating] = useState(false);
|
||||||
const [ideaTab, setIdeaTab] = useState<"pending" | "rejected">("pending");
|
const [ideaTab, setIdeaTab] = useState<"pending" | "rejected">("pending");
|
||||||
const [scriptTab, setScriptTab] = useState<"draft" | "approved" | "rejected">("draft");
|
const [scriptTab, setScriptTab] = useState<"draft" | "approved" | "rejected">("draft");
|
||||||
|
const [ideaCategory, setIdeaCategory] = useState<"brands" | "ai-agent-tips">("brands");
|
||||||
const [rejectModal, setRejectModal] = useState<{ type: "idea" | "script"; id: string; title: string } | null>(null);
|
const [rejectModal, setRejectModal] = useState<{ type: "idea" | "script"; id: string; title: string } | null>(null);
|
||||||
const rejectRef = React.useRef<HTMLTextAreaElement>(null);
|
const rejectRef = React.useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ export default function YouTubePage() {
|
|||||||
const res = await fetch("/api/youtube/generate", {
|
const res = await fetch("/api/youtube/generate", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ topic: `${idea.title}. Hook angle: ${idea.hook}. Lesson: ${idea.angle}`, sourceUrl: idea.sourceUrl, sourceSummary: idea.sourceSummary }),
|
body: JSON.stringify({ topic: `${idea.title}. Hook angle: ${idea.hook}. Lesson: ${idea.angle}`, sourceUrl: idea.sourceUrl, sourceSummary: idea.sourceSummary, category: idea.category || ideaCategory }),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -629,11 +632,24 @@ export default function YouTubePage() {
|
|||||||
}}
|
}}
|
||||||
>{batchGenerating ? `✨ Generating ${selectedIdeas.size}...` : `Generate ${selectedIdeas.size} Script${selectedIdeas.size > 1 ? "s" : ""}`}</Button>
|
>{batchGenerating ? `✨ Generating ${selectedIdeas.size}...` : `Generate ${selectedIdeas.size} Script${selectedIdeas.size > 1 ? "s" : ""}`}</Button>
|
||||||
)}
|
)}
|
||||||
|
<select
|
||||||
|
value={ideaCategory}
|
||||||
|
onChange={(e) => setIdeaCategory(e.target.value as "brands" | "ai-agent-tips")}
|
||||||
|
className="text-xs bg-[var(--surface)] border border-[var(--border)] rounded-lg px-2 py-1.5 text-[var(--muted)]"
|
||||||
|
title="Content category for idea generation"
|
||||||
|
>
|
||||||
|
<option value="brands">Brands & Founders</option>
|
||||||
|
<option value="ai-agent-tips">AI Agent Tips</option>
|
||||||
|
</select>
|
||||||
<Button size="sm" variant="ghost" disabled={generatingIdeas}
|
<Button size="sm" variant="ghost" disabled={generatingIdeas}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setGeneratingIdeas(true);
|
setGeneratingIdeas(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/youtube/ideas/generate", { method: "POST" });
|
const res = await fetch("/api/youtube/ideas/generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ category: ideaCategory }),
|
||||||
|
});
|
||||||
if (res.ok) fetchIdeas();
|
if (res.ok) fetchIdeas();
|
||||||
} catch { /* empty */ }
|
} catch { /* empty */ }
|
||||||
setGeneratingIdeas(false);
|
setGeneratingIdeas(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user