@@ -4,16 +4,13 @@ import type { NextRequest } from "next/server";
44
55import { generateWithGemini , stripCodeFences } from "@/lib/gemini" ;
66import { writeClient } from "@/lib/sanity-write-client" ;
7+ import { discoverTrends , type TrendResult } from "@/lib/services/trend-discovery" ;
8+ import { conductResearch , type ResearchPayload } from "@/lib/services/research" ;
79
810// ---------------------------------------------------------------------------
911// Types
1012// ---------------------------------------------------------------------------
1113
12- interface RSSItem {
13- title : string ;
14- url : string ;
15- }
16-
1714interface ScriptScene {
1815 sceneNumber : number ;
1916 sceneType : "narration" | "code" | "list" | "comparison" | "mockup" ;
@@ -62,122 +59,107 @@ interface CriticResult {
6259}
6360
6461// ---------------------------------------------------------------------------
65- // RSS Feed Helpers
62+ // Fallback topics (used when discoverTrends returns empty)
6663// ---------------------------------------------------------------------------
6764
68- const RSS_FEEDS = [
69- "https://hnrss.org/newest?q=javascript+OR+react+OR+nextjs+OR+typescript&points=50" ,
70- "https://dev.to/feed/tag/webdev" ,
71- ] ;
72-
73- const FALLBACK_TOPICS : RSSItem [ ] = [
65+ const FALLBACK_TRENDS : TrendResult [ ] = [
7466 {
75- title : "React Server Components: The Future of Web Development" ,
76- url : "https://react.dev/blog" ,
67+ topic : "React Server Components: The Future of Web Development" ,
68+ slug : "react-server-components" ,
69+ score : 80 ,
70+ signals : [ { source : "blog" , title : "React Server Components" , url : "https://react.dev/blog" , score : 80 } ] ,
71+ whyTrending : "Major shift in React architecture" ,
72+ suggestedAngle : "Explain what RSC changes for everyday React developers" ,
7773 } ,
7874 {
79- title : "TypeScript 5.x: New Features Every Developer Should Know" ,
80- url : "https://devblogs.microsoft.com/typescript/" ,
75+ topic : "TypeScript 5.x: New Features Every Developer Should Know" ,
76+ slug : "typescript-5x-features" ,
77+ score : 75 ,
78+ signals : [ { source : "blog" , title : "TypeScript 5.x" , url : "https://devblogs.microsoft.com/typescript/" , score : 75 } ] ,
79+ whyTrending : "New TypeScript release with major DX improvements" ,
80+ suggestedAngle : "Walk through the top 5 new features with code examples" ,
8181 } ,
8282 {
83- title : "Next.js App Router Best Practices for 2025" ,
84- url : "https://nextjs.org/blog" ,
83+ topic : "Next.js App Router Best Practices for 2025" ,
84+ slug : "nextjs-app-router-2025" ,
85+ score : 70 ,
86+ signals : [ { source : "blog" , title : "Next.js App Router" , url : "https://nextjs.org/blog" , score : 70 } ] ,
87+ whyTrending : "App Router adoption is accelerating" ,
88+ suggestedAngle : "Common pitfalls and how to avoid them" ,
8589 } ,
8690 {
87- title : "The State of CSS in 2025: Container Queries, Layers, and More" ,
88- url : "https://web.dev/blog" ,
91+ topic : "The State of CSS in 2025: Container Queries, Layers, and More" ,
92+ slug : "css-2025-state" ,
93+ score : 65 ,
94+ signals : [ { source : "blog" , title : "CSS 2025" , url : "https://web.dev/blog" , score : 65 } ] ,
95+ whyTrending : "CSS has gained powerful new features" ,
96+ suggestedAngle : "Demo the top 3 CSS features you should be using today" ,
8997 } ,
9098 {
91- title : "WebAssembly is Changing How We Build Web Apps" ,
92- url : "https://webassembly.org/" ,
99+ topic : "WebAssembly is Changing How We Build Web Apps" ,
100+ slug : "webassembly-web-apps" ,
101+ score : 60 ,
102+ signals : [ { source : "blog" , title : "WebAssembly" , url : "https://webassembly.org/" , score : 60 } ] ,
103+ whyTrending : "WASM adoption growing in production apps" ,
104+ suggestedAngle : "Real-world use cases where WASM outperforms JS" ,
93105 } ,
94106] ;
95107
96- function extractRSSItems ( xml : string ) : RSSItem [ ] {
97- const items : RSSItem [ ] = [ ] ;
98- const itemRegex = / < i t e m > ( [ \s \S ] * ?) < \/ i t e m > / gi;
99- let itemMatch : RegExpExecArray | null ;
108+ // ---------------------------------------------------------------------------
109+ // Gemini Script Generation
110+ // ---------------------------------------------------------------------------
100111
101- while ( ( itemMatch = itemRegex . exec ( xml ) ) !== null ) {
102- const block = itemMatch [ 1 ] ;
112+ const SYSTEM_INSTRUCTION =
113+ "You are a content strategist for CodingCat.dev, a web development education channel. You create engaging, Cleo Abram-style explainer video scripts that are educational, energetic, and concise (60-90 seconds)." ;
103114
104- const titleMatch = block . match ( / < t i t l e > < ! \[ C D A T A \[ ( .* ?) \] \] > < \/ t i t l e > / ) ;
105- const titleAlt = block . match ( / < t i t l e > ( .* ?) < \/ t i t l e > / ) ;
106- const title = titleMatch ?. [ 1 ] ?? titleAlt ?. [ 1 ] ?? "" ;
115+ function buildPrompt ( trends : TrendResult [ ] , research ?: ResearchPayload ) : string {
116+ const topicList = trends
117+ . map ( ( t , i ) => `${ i + 1 } . "${ t . topic } " (score: ${ t . score } ) — ${ t . whyTrending } \n Sources: ${ t . signals . map ( s => s . url ) . join ( ", " ) } ` )
118+ . join ( "\n" ) ;
107119
108- const linkMatch = block . match ( / < l i n k > ( .* ?) < \/ l i n k > / ) ;
109- const url = linkMatch ?. [ 1 ] ?? "" ;
120+ // If we have research data, include it as enrichment
121+ let researchContext = "" ;
122+ if ( research ) {
123+ researchContext = `\n\n## Research Data (use this to create an informed, accurate script)\n\n` ;
124+ researchContext += `### Briefing\n${ research . briefing } \n\n` ;
110125
111- if ( title && url ) {
112- items . push ( { title : title . trim ( ) , url : url . trim ( ) } ) ;
126+ if ( research . talkingPoints . length > 0 ) {
127+ researchContext += `### Key Talking Points\n ${ research . talkingPoints . map ( ( tp , i ) => ` ${ i + 1 } . ${ tp } ` ) . join ( "\n" ) } \n\n` ;
113128 }
114- }
115-
116- return items ;
117- }
118129
119- async function fetchTrendingTopics ( ) : Promise < RSSItem [ ] > {
120- const allItems : RSSItem [ ] = [ ] ;
130+ if ( research . codeExamples . length > 0 ) {
131+ researchContext += `### Code Examples (use these in "code" scenes)\n` ;
132+ for ( const ex of research . codeExamples . slice ( 0 , 5 ) ) {
133+ researchContext += `\`\`\`${ ex . language } \n${ ex . snippet } \n\`\`\`\nContext: ${ ex . context } \n\n` ;
134+ }
135+ }
121136
122- const results = await Promise . allSettled (
123- RSS_FEEDS . map ( async ( feedUrl ) => {
124- const controller = new AbortController ( ) ;
125- const timeout = setTimeout ( ( ) => controller . abort ( ) , 10_000 ) ;
126- try {
127- const res = await fetch ( feedUrl , { signal : controller . signal } ) ;
128- if ( ! res . ok ) {
129- console . warn (
130- `[CRON/ingest] RSS fetch failed for ${ feedUrl } : ${ res . status } ` ,
131- ) ;
132- return [ ] ;
137+ if ( research . comparisonData && research . comparisonData . length > 0 ) {
138+ researchContext += `### Comparison Data (use in "comparison" scenes)\n` ;
139+ for ( const comp of research . comparisonData ) {
140+ researchContext += `${ comp . leftLabel } vs ${ comp . rightLabel } :\n` ;
141+ for ( const row of comp . rows ) {
142+ researchContext += ` - ${ row . left } | ${ row . right } \n` ;
133143 }
134- const xml = await res . text ( ) ;
135- return extractRSSItems ( xml ) ;
136- } finally {
137- clearTimeout ( timeout ) ;
144+ researchContext += "\n" ;
138145 }
139- } ) ,
140- ) ;
141-
142- for ( const result of results ) {
143- if ( result . status === "fulfilled" ) {
144- allItems . push ( ...result . value ) ;
145- } else {
146- console . warn ( "[CRON/ingest] RSS feed error:" , result . reason ) ;
147146 }
148- }
149147
150- const seen = new Set < string > ( ) ;
151- const unique = allItems . filter ( ( item ) => {
152- const key = item . title . toLowerCase ( ) ;
153- if ( seen . has ( key ) ) return false ;
154- seen . add ( key ) ;
155- return true ;
156- } ) ;
148+ if ( research . sceneHints . length > 0 ) {
149+ researchContext += `### Scene Type Suggestions\n` ;
150+ for ( const hint of research . sceneHints ) {
151+ researchContext += `- ${ hint . suggestedSceneType } : ${ hint . reason } \n` ;
152+ }
153+ }
157154
158- if ( unique . length === 0 ) {
159- console . warn ( "[CRON/ingest] No RSS items fetched, using fallback topics" ) ;
160- return FALLBACK_TOPICS ;
155+ if ( research . infographicPath ) {
156+ researchContext += `\n### Infographic Available\nAn infographic has been generated for this topic. Use sceneType "narration" with bRollUrl pointing to the infographic for at least one scene.\n` ;
157+ }
161158 }
162159
163- return unique . slice ( 0 , 10 ) ;
164- }
165-
166- // ---------------------------------------------------------------------------
167- // Gemini Script Generation
168- // ---------------------------------------------------------------------------
169-
170- const SYSTEM_INSTRUCTION =
171- "You are a content strategist for CodingCat.dev, a web development education channel. You create engaging, Cleo Abram-style explainer video scripts that are educational, energetic, and concise (60-90 seconds)." ;
172-
173- function buildPrompt ( topics : RSSItem [ ] ) : string {
174- const topicList = topics
175- . map ( ( t , i ) => `${ i + 1 } . "${ t . title } " — ${ t . url } ` )
176- . join ( "\n" ) ;
177-
178160 return `Here are today's trending web development topics:
179161
180- ${ topicList }
162+ ${ topicList } ${ researchContext }
181163
182164Pick the MOST interesting and timely topic for a short explainer video (60-90 seconds). Then generate a complete video script as JSON.
183165
@@ -334,6 +316,8 @@ Respond with ONLY the JSON object.`,
334316async function createSanityDocuments (
335317 script : GeneratedScript ,
336318 criticResult : CriticResult ,
319+ trends : TrendResult [ ] ,
320+ research ?: ResearchPayload ,
337321) {
338322 const isFlagged = criticResult . score < 50 ;
339323
@@ -369,6 +353,9 @@ async function createSanityDocuments(
369353 ...( isFlagged && {
370354 flaggedReason : `Quality score ${ criticResult . score } /100. Issues: ${ criticResult . issues . join ( "; " ) || "Low quality score" } ` ,
371355 } ) ,
356+ trendScore : trends [ 0 ] ?. score ,
357+ trendSources : trends [ 0 ] ?. signals . map ( s => s . source ) . join ( ", " ) ,
358+ researchNotebookId : research ?. notebookId ,
372359 } ) ;
373360
374361 console . log ( `[CRON/ingest] Created automatedVideo: ${ automatedVideo . _id } ` ) ;
@@ -394,12 +381,38 @@ export async function GET(request: NextRequest) {
394381 }
395382
396383 try {
397- console . log ( "[CRON/ingest] Fetching trending topics..." ) ;
398- const topics = await fetchTrendingTopics ( ) ;
399- console . log ( `[CRON/ingest] Found ${ topics . length } topics` ) ;
384+ // Step 1: Discover trending topics (replaces fetchTrendingTopics)
385+ console . log ( "[CRON/ingest] Discovering trending topics..." ) ;
386+ let trends : TrendResult [ ] ;
387+ try {
388+ trends = await discoverTrends ( { lookbackDays : 7 , maxTopics : 10 } ) ;
389+ console . log ( `[CRON/ingest] Found ${ trends . length } trending topics` ) ;
390+ } catch ( err ) {
391+ console . warn ( "[CRON/ingest] Trend discovery failed, using fallback topics:" , err ) ;
392+ trends = [ ] ;
393+ }
394+
395+ // Fall back to hardcoded topics if discovery returns empty or failed
396+ if ( trends . length === 0 ) {
397+ console . warn ( "[CRON/ingest] No trends discovered, using fallback topics" ) ;
398+ trends = FALLBACK_TRENDS ;
399+ }
400+
401+ // Step 2: Optional deep research on top topic
402+ let research : ResearchPayload | undefined ;
403+ if ( process . env . ENABLE_NOTEBOOKLM_RESEARCH === "true" ) {
404+ console . log ( `[CRON/ingest] Conducting research on: "${ trends [ 0 ] . topic } "...` ) ;
405+ try {
406+ research = await conductResearch ( trends [ 0 ] . topic ) ;
407+ console . log ( `[CRON/ingest] Research complete: ${ research . sources . length } sources, ${ research . sceneHints . length } scene hints` ) ;
408+ } catch ( err ) {
409+ console . warn ( "[CRON/ingest] Research failed, continuing without:" , err ) ;
410+ }
411+ }
400412
413+ // Step 3: Generate script with Gemini (enriched with research)
401414 console . log ( "[CRON/ingest] Generating script with Gemini..." ) ;
402- const prompt = buildPrompt ( topics ) ;
415+ const prompt = buildPrompt ( trends , research ) ;
403416 const rawResponse = await generateWithGemini ( prompt , SYSTEM_INSTRUCTION ) ;
404417
405418 let script : GeneratedScript ;
@@ -430,7 +443,7 @@ export async function GET(request: NextRequest) {
430443 ) ;
431444
432445 console . log ( "[CRON/ingest] Creating Sanity documents..." ) ;
433- const result = await createSanityDocuments ( script , criticResult ) ;
446+ const result = await createSanityDocuments ( script , criticResult , trends , research ) ;
434447
435448 console . log ( "[CRON/ingest] Done!" , result ) ;
436449
@@ -439,7 +452,9 @@ export async function GET(request: NextRequest) {
439452 ...result ,
440453 title : script . title ,
441454 criticScore : criticResult . score ,
442- topicCount : topics . length ,
455+ trendCount : trends . length ,
456+ trendScore : trends [ 0 ] ?. score ,
457+ researchEnabled : ! ! research ,
443458 } ) ;
444459 } catch ( err ) {
445460 console . error ( "[CRON/ingest] Unexpected error:" , err ) ;
0 commit comments