Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions lib/gemini.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { GoogleGenerativeAI } from "@google/generative-ai";
import { GoogleGenAI } from "@google/genai";
import { getConfigValue } from "@/lib/config";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
let _ai: GoogleGenAI | null = null;

/** Lazy-initialize the GoogleGenAI client (avoids crash at import time if GEMINI_API_KEY is missing). */
function getAI(): GoogleGenAI {
if (!_ai) {
const apiKey = process.env.GEMINI_API_KEY || "";
_ai = new GoogleGenAI({ apiKey });
}
return _ai;
}

/**
* Generate text content using Gemini Flash.
Expand All @@ -14,13 +23,13 @@ export async function generateWithGemini(
systemInstruction?: string,
): Promise<string> {
const geminiModel = await getConfigValue("pipeline_config", "geminiModel", "gemini-2.0-flash");
const model = genAI.getGenerativeModel({
const ai = getAI();
const response = await ai.models.generateContent({
model: geminiModel,
...(systemInstruction && { systemInstruction }),
contents: prompt,
...(systemInstruction && { config: { systemInstruction } }),
});
const result = await model.generateContent(prompt);
const response = result.response;
return response.text();
return response.text ?? "";
}

/**
Expand Down
17 changes: 7 additions & 10 deletions lib/sponsor/gemini-intent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GoogleGenerativeAI } from '@google/generative-ai'
import { GoogleGenAI } from '@google/genai'
import { getConfigValue } from '@/lib/config'

const SPONSORSHIP_TIERS = [
Expand All @@ -17,10 +17,6 @@ export interface SponsorIntent {
urgency: 'low' | 'medium' | 'high'
}

/**
* Uses Gemini Flash to parse inbound sponsor emails/messages
* and extract structured data for creating a sponsorLead.
*/
export async function extractSponsorIntent(message: string): Promise<SponsorIntent> {
const apiKey = process.env.GEMINI_API_KEY
if (!apiKey) {
Expand All @@ -35,8 +31,7 @@ export async function extractSponsorIntent(message: string): Promise<SponsorInte
}

const geminiModel = await getConfigValue('pipeline_config', 'geminiModel', 'gemini-2.0-flash')
const genAI = new GoogleGenerativeAI(apiKey)
const model = genAI.getGenerativeModel({ model: geminiModel })
const ai = new GoogleGenAI({ apiKey })

const prompt = `You are analyzing an inbound sponsorship inquiry for CodingCat.dev, a developer education platform with YouTube videos, podcasts, blog posts, and newsletters.

Expand All @@ -61,9 +56,11 @@ Message to analyze:
${message}`

try {
const result = await model.generateContent(prompt)
const response = result.response
const text = response.text().trim()
const response = await ai.models.generateContent({
model: geminiModel,
contents: prompt,
})
const text = (response.text ?? '').trim()

// Strip any markdown code fences if present
const jsonStr = text.replace(/^```json?\n?/i, '').replace(/\n?```$/i, '').trim()
Expand Down
17 changes: 7 additions & 10 deletions lib/sponsor/gemini-outreach.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GoogleGenerativeAI } from '@google/generative-ai'
import { GoogleGenAI } from '@google/genai'
import { getConfigValue } from '@/lib/config'

export interface SponsorPoolEntry {
Expand Down Expand Up @@ -28,10 +28,6 @@ CodingCat.dev Sponsorship Tiers:
Our audience: 50K+ developers interested in web development, JavaScript/TypeScript, React, Next.js, and modern dev tools.
`.trim()

/**
* Uses Gemini to generate a personalized cold outreach email
* for a potential sponsor from the sponsor pool.
*/
export async function generateOutreachEmail(
sponsor: SponsorPoolEntry,
rateCard: string = DEFAULT_RATE_CARD
Expand All @@ -42,9 +38,8 @@ export async function generateOutreachEmail(
return getTemplateEmail(sponsor, rateCard)
}

const genAI = new GoogleGenerativeAI(apiKey)
const ai = new GoogleGenAI({ apiKey })
const geminiModel = await getConfigValue("pipeline_config", "geminiModel", "gemini-2.0-flash");
const model = genAI.getGenerativeModel({ model: geminiModel })

const optOutUrl = sponsor.optOutToken
? `${process.env.NEXT_PUBLIC_URL || 'https://codingcat.dev'}/api/sponsor/opt-out?token=${sponsor.optOutToken}`
Expand Down Expand Up @@ -74,9 +69,11 @@ Respond ONLY with valid JSON, no markdown formatting:
{"subject": "...", "body": "..."}`

try {
const result = await model.generateContent(prompt)
const response = result.response
const text = response.text().trim()
const response = await ai.models.generateContent({
model: geminiModel,
contents: prompt,
})
const text = (response.text ?? '').trim()

const jsonStr = text.replace(/^```json?\n?/i, '').replace(/\n?```$/i, '').trim()
const parsed = JSON.parse(jsonStr) as OutreachEmail
Expand Down
4 changes: 4 additions & 0 deletions lib/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export interface PipelineConfig {
youtubeUploadVisibility: string;
youtubeChannelId: string;
enableNotebookLmResearch: boolean;
enableDeepResearch: boolean;
deepResearchAgent: string;
deepResearchPromptTemplate: string;
infographicModel: string;
qualityThreshold: number;
stuckTimeoutMinutes: number;
maxIdeasPerRun: number;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@codingcatdev/sanity-plugin-podcast-rss": "^1.0.0",
"@google/generative-ai": "^0.24.1",
"@google/genai": "^1.0.0",
"@hookform/resolvers": "^5.2.2",
"@marsidev/react-turnstile": "^1.4.2",
"@portabletext/block-tools": "^5.0.5",
Expand Down
30 changes: 29 additions & 1 deletion sanity/schemas/singletons/pipelineConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default defineType({
name: "youtubeChannelId",
title: "YouTube Channel ID",
type: "string",
description: "Your YouTube channel ID \u2014 used for analytics and upload targeting",
description: "Your YouTube channel ID used for analytics and upload targeting",
initialValue: "",
}),
defineField({
Expand All @@ -44,6 +44,34 @@ export default defineType({
description: "When enabled, the ingest cron creates a NotebookLM notebook for deep research before script generation. Requires NOTEBOOKLM_AUTH_JSON env var",
initialValue: false,
}),
defineField({
name: "enableDeepResearch",
title: "Enable Deep Research",
type: "boolean",
description: "When enabled, the ingest cron uses Gemini Deep Research API for comprehensive topic research before script generation. Replaces NotebookLM research.",
initialValue: false,
}),
defineField({
name: "deepResearchAgent",
title: "Deep Research Agent",
type: "string",
description: "The Gemini Deep Research agent model ID. This is the agent used for autonomous web research via the Interactions API",
initialValue: "deep-research-pro-preview-12-2025",
}),
defineField({
name: "deepResearchPromptTemplate",
title: "Deep Research Prompt Template",
type: "text",
description: "Template for Deep Research queries. Use {topic} as placeholder for the trend topic. Sent to the Deep Research agent for autonomous web research",
initialValue: "Research comprehensively: \"{topic}\"\n\nFocus areas:\n- What is it and why does it matter?\n- How does it work technically?\n- Key features and capabilities\n- Comparison with alternatives\n- Getting started guide\n- Common pitfalls and best practices\n\nTarget audience: Web developers learning new tech.\nTone: Educational, accessible, engaging.",
}),
defineField({
name: "infographicModel",
title: "Infographic Model",
type: "string",
description: "Model used for generating brand-consistent infographics from research data. Imagen 4 Fast ($0.02/image) supports seed-based reproducibility",
initialValue: "imagen-4-fast",
}),
defineField({
name: "qualityThreshold",
title: "Quality Threshold",
Expand Down
Loading