|
|
|
|
@@ -15,6 +15,18 @@ const openai = new OpenAI({
|
|
|
|
|
apiKey: process.env.OPENAI_API_KEY
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
type ProductProfile = {
|
|
|
|
|
productName: string
|
|
|
|
|
category: string
|
|
|
|
|
primaryJTBD: string
|
|
|
|
|
targetPersona: string
|
|
|
|
|
scopeBoundary: string
|
|
|
|
|
nonGoals: string[]
|
|
|
|
|
differentiators: string[]
|
|
|
|
|
evidence: { claim: string; snippet: string }[]
|
|
|
|
|
confidence: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function aiGenerate<T>(prompt: string, systemPrompt: string, temperature: number = 0.3): Promise<T> {
|
|
|
|
|
const response = await openai.chat.completions.create({
|
|
|
|
|
model: 'gpt-4o-mini',
|
|
|
|
|
@@ -39,7 +51,53 @@ async function aiGenerate<T>(prompt: string, systemPrompt: string, temperature:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function extractFeatures(content: ScrapedContent): Promise<Feature[]> {
|
|
|
|
|
function buildEvidenceContext(content: ScrapedContent) {
|
|
|
|
|
return [
|
|
|
|
|
`Title: ${content.title}`,
|
|
|
|
|
`Description: ${content.metaDescription}`,
|
|
|
|
|
`Headings: ${content.headings.slice(0, 20).join('\n')}`,
|
|
|
|
|
`Feature Lists: ${content.featureList.slice(0, 20).join('\n')}`,
|
|
|
|
|
`Paragraphs: ${content.paragraphs.slice(0, 12).join('\n\n')}`,
|
|
|
|
|
].join('\n\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function extractProductProfile(content: ScrapedContent, extraPrompt?: string): Promise<ProductProfile> {
|
|
|
|
|
const systemPrompt = `You are a strict product analyst. Only use provided evidence. If uncertain, answer "unknown" and lower confidence. Return JSON only.`
|
|
|
|
|
const prompt = `Analyze the product using evidence only.
|
|
|
|
|
|
|
|
|
|
${buildEvidenceContext(content)}
|
|
|
|
|
|
|
|
|
|
Return JSON:
|
|
|
|
|
{
|
|
|
|
|
"productName": "...",
|
|
|
|
|
"category": "...",
|
|
|
|
|
"primaryJTBD": "...",
|
|
|
|
|
"targetPersona": "...",
|
|
|
|
|
"scopeBoundary": "...",
|
|
|
|
|
"nonGoals": ["..."],
|
|
|
|
|
"differentiators": ["..."],
|
|
|
|
|
"evidence": [{"claim": "...", "snippet": "..."}],
|
|
|
|
|
"confidence": 0.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rules:
|
|
|
|
|
- "category" should be a concrete market category, not "software/tool/platform".
|
|
|
|
|
- "scopeBoundary" must state what the product does NOT do.
|
|
|
|
|
- "nonGoals" should be explicit exclusions inferred from evidence.
|
|
|
|
|
- "evidence.snippet" must quote or paraphrase short evidence from the text above.
|
|
|
|
|
${extraPrompt ? `\nUser guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<ProductProfile>(prompt, systemPrompt, 0.2)
|
|
|
|
|
return {
|
|
|
|
|
...result,
|
|
|
|
|
nonGoals: result.nonGoals?.slice(0, 6) ?? [],
|
|
|
|
|
differentiators: result.differentiators?.slice(0, 6) ?? [],
|
|
|
|
|
evidence: result.evidence?.slice(0, 6) ?? [],
|
|
|
|
|
confidence: typeof result.confidence === "number" ? result.confidence : 0.3,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function extractFeatures(content: ScrapedContent, extraPrompt?: string): Promise<Feature[]> {
|
|
|
|
|
const systemPrompt = `Extract EVERY feature from website content. Be exhaustive.`
|
|
|
|
|
const prompt = `Extract features from:
|
|
|
|
|
Title: ${content.title}
|
|
|
|
|
@@ -49,19 +107,79 @@ Paragraphs: ${content.paragraphs.slice(0, 10).join('\n\n')}
|
|
|
|
|
Feature Lists: ${content.featureList.slice(0, 15).join('\n')}
|
|
|
|
|
|
|
|
|
|
Return JSON: {"features": [{"name": "...", "description": "...", "benefits": ["..."], "useCases": ["..."]}]}
|
|
|
|
|
Aim for 10-15 features.`
|
|
|
|
|
Aim for 10-15 features.
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ features: Feature[] }>(prompt, systemPrompt, 0.4)
|
|
|
|
|
return result.features.slice(0, 20)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function identifyCompetitors(content: ScrapedContent): Promise<Competitor[]> {
|
|
|
|
|
const systemPrompt = `Identify real, named competitors. Use actual company/product names like "Asana", "Jira", "Monday.com", "Trello", "Notion". Never use generic names like "Competitor A".`
|
|
|
|
|
async function generateCompetitorCandidates(
|
|
|
|
|
profile: ProductProfile,
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<{ candidates: { name: string; type: "direct" | "adjacent" | "generic"; rationale: string; confidence: number }[] }> {
|
|
|
|
|
const systemPrompt = `Generate candidate competitors based only on the product profile. Return JSON only.`
|
|
|
|
|
const prompt = `Product profile:
|
|
|
|
|
Name: ${profile.productName}
|
|
|
|
|
Category: ${profile.category}
|
|
|
|
|
JTBD: ${profile.primaryJTBD}
|
|
|
|
|
Target persona: ${profile.targetPersona}
|
|
|
|
|
Scope boundary: ${profile.scopeBoundary}
|
|
|
|
|
Non-goals: ${profile.nonGoals.join(", ") || "unknown"}
|
|
|
|
|
Differentiators: ${profile.differentiators.join(", ") || "unknown"}
|
|
|
|
|
|
|
|
|
|
const prompt = `Identify 5-6 real competitors for: ${content.title}
|
|
|
|
|
Description: ${content.metaDescription}
|
|
|
|
|
Evidence (for context):
|
|
|
|
|
${profile.evidence.map(e => `- ${e.claim}: ${e.snippet}`).join("\n")}
|
|
|
|
|
|
|
|
|
|
Return EXACT JSON format:
|
|
|
|
|
Rules:
|
|
|
|
|
- Output real product/company names only.
|
|
|
|
|
- Classify as "direct" if same JTBD + same persona + same category.
|
|
|
|
|
- "adjacent" if overlap partially.
|
|
|
|
|
- "generic" for broad tools people misuse for this (only include if evidence suggests).
|
|
|
|
|
- Avoid broad suites unless the category is that suite.
|
|
|
|
|
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}\n` : ""}
|
|
|
|
|
|
|
|
|
|
Return JSON:
|
|
|
|
|
{
|
|
|
|
|
"candidates": [
|
|
|
|
|
{ "name": "Product", "type": "direct|adjacent|generic", "rationale": "...", "confidence": 0.0 }
|
|
|
|
|
]
|
|
|
|
|
}`
|
|
|
|
|
|
|
|
|
|
return await aiGenerate<{ candidates: { name: string; type: "direct" | "adjacent" | "generic"; rationale: string; confidence: number }[] }>(
|
|
|
|
|
prompt,
|
|
|
|
|
systemPrompt,
|
|
|
|
|
0.2
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function selectDirectCompetitors(
|
|
|
|
|
profile: ProductProfile,
|
|
|
|
|
candidates: { name: string; type: "direct" | "adjacent" | "generic"; rationale: string; confidence: number }[],
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<Competitor[]> {
|
|
|
|
|
const systemPrompt = `You are a strict verifier. Only accept direct competitors. Return JSON only.`
|
|
|
|
|
const prompt = `Product profile:
|
|
|
|
|
Name: ${profile.productName}
|
|
|
|
|
Category: ${profile.category}
|
|
|
|
|
JTBD: ${profile.primaryJTBD}
|
|
|
|
|
Target persona: ${profile.targetPersona}
|
|
|
|
|
Scope boundary: ${profile.scopeBoundary}
|
|
|
|
|
Non-goals: ${profile.nonGoals.join(", ") || "unknown"}
|
|
|
|
|
Differentiators: ${profile.differentiators.join(", ") || "unknown"}
|
|
|
|
|
|
|
|
|
|
Candidates:
|
|
|
|
|
${candidates.map(c => `- ${c.name} (${c.type}) : ${c.rationale}`).join("\n")}
|
|
|
|
|
|
|
|
|
|
Rules:
|
|
|
|
|
- Only keep DIRECT competitors (same JTBD + persona + category).
|
|
|
|
|
- Reject "generic" tools unless the category itself is generic.
|
|
|
|
|
- Provide 3-6 competitors. If fewer, include the closest adjacent but label as direct only if truly overlapping.
|
|
|
|
|
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}\n` : ""}
|
|
|
|
|
|
|
|
|
|
Return JSON:
|
|
|
|
|
{
|
|
|
|
|
"competitors": [
|
|
|
|
|
{
|
|
|
|
|
@@ -72,28 +190,35 @@ Return EXACT JSON format:
|
|
|
|
|
"theirWeakness": "Their main weakness"
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ competitors: Competitor[] }>(prompt, systemPrompt, 0.2)
|
|
|
|
|
return result.competitors
|
|
|
|
|
.map(c => ({
|
|
|
|
|
...c,
|
|
|
|
|
name: c.name.replace(/^Competitor\s+[A-Z]$/i, 'Alternative Solution').replace(/^Generic\s+/i, '')
|
|
|
|
|
}))
|
|
|
|
|
.filter(c => c.name.length > 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Include: Direct competitors (same space), Big players, Popular alternatives, Tools people misuse for this. Use ONLY real product names.`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ competitors: Competitor[] }>(prompt, systemPrompt, 0.3)
|
|
|
|
|
|
|
|
|
|
// Validate competitor names aren't generic
|
|
|
|
|
return result.competitors.map(c => ({
|
|
|
|
|
...c,
|
|
|
|
|
name: c.name.replace(/^Competitor\s+[A-Z]$/i, 'Alternative Solution').replace(/^Generic\s+/i, '')
|
|
|
|
|
})).filter(c => c.name.length > 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function generateKeywords(features: Feature[], content: ScrapedContent, competitors: Competitor[]): Promise<Keyword[]> {
|
|
|
|
|
async function generateKeywords(
|
|
|
|
|
features: Feature[],
|
|
|
|
|
content: ScrapedContent,
|
|
|
|
|
competitors: Competitor[],
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<Keyword[]> {
|
|
|
|
|
const systemPrompt = `Generate search-ready phrases users would actually type.`
|
|
|
|
|
|
|
|
|
|
const featuresText = features.map(f => f.name).join(', ')
|
|
|
|
|
const competitorNames = competitors.map(c => c.name).filter(n => n.length > 1).join(', ') || 'Jira, Asana, Monday, Trello'
|
|
|
|
|
const competitorNames = competitors.map(c => c.name).filter(n => n.length > 1).join(', ')
|
|
|
|
|
|
|
|
|
|
const differentiatorGuidance = competitorNames
|
|
|
|
|
? `Generate 20+ differentiator phrases comparing to: ${competitorNames}`
|
|
|
|
|
: `If no competitors are provided, do not invent them. Reduce differentiator share to 5% using generic phrases like "alternatives to X category".`
|
|
|
|
|
|
|
|
|
|
const prompt = `Generate 60-80 search phrases for: ${content.title}
|
|
|
|
|
Features: ${featuresText}
|
|
|
|
|
Competitors: ${competitorNames}
|
|
|
|
|
Competitors: ${competitorNames || "None"}
|
|
|
|
|
|
|
|
|
|
CRITICAL - Follow this priority:
|
|
|
|
|
1. 60% 2-4 word phrases (e.g., "client onboarding checklist", "bug triage workflow")
|
|
|
|
|
@@ -102,7 +227,8 @@ CRITICAL - Follow this priority:
|
|
|
|
|
|
|
|
|
|
Return JSON: {"keywords": [{"term": "phrase", "type": "differentiator|product|feature|problem|solution|competitor", "searchVolume": "high|medium|low", "intent": "informational|navigational|transactional", "funnel": "awareness|consideration|decision", "emotionalIntensity": "frustrated|curious|ready"}]}
|
|
|
|
|
|
|
|
|
|
Generate 20+ differentiator phrases comparing to: ${competitorNames}`
|
|
|
|
|
${differentiatorGuidance}
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ keywords: Keyword[] }>(prompt, systemPrompt, 0.4)
|
|
|
|
|
|
|
|
|
|
@@ -143,36 +269,52 @@ Generate 20+ differentiator phrases comparing to: ${competitorNames}`
|
|
|
|
|
}).slice(0, 80)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function identifyProblems(features: Feature[], content: ScrapedContent): Promise<Problem[]> {
|
|
|
|
|
async function identifyProblems(
|
|
|
|
|
features: Feature[],
|
|
|
|
|
content: ScrapedContent,
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<Problem[]> {
|
|
|
|
|
const systemPrompt = `Identify problems using JTBD framework.`
|
|
|
|
|
const prompt = `Identify 8-12 problems solved by: ${features.map(f => f.name).join(', ')}
|
|
|
|
|
Content: ${content.rawText.slice(0, 3000)}
|
|
|
|
|
|
|
|
|
|
Return JSON: {"problems": [{"problem": "...", "severity": "high|medium|low", "currentWorkarounds": ["..."], "emotionalImpact": "...", "searchTerms": ["..."]}]}`
|
|
|
|
|
Return JSON: {"problems": [{"problem": "...", "severity": "high|medium|low", "currentWorkarounds": ["..."], "emotionalImpact": "...", "searchTerms": ["..."]}]}
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ problems: Problem[] }>(prompt, systemPrompt, 0.4)
|
|
|
|
|
return result.problems
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function generatePersonas(content: ScrapedContent, problems: Problem[]): Promise<Persona[]> {
|
|
|
|
|
async function generatePersonas(
|
|
|
|
|
content: ScrapedContent,
|
|
|
|
|
problems: Problem[],
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<Persona[]> {
|
|
|
|
|
const systemPrompt = `Create diverse user personas with search behavior.`
|
|
|
|
|
const prompt = `Create 4-5 personas for: ${content.title}
|
|
|
|
|
Description: ${content.metaDescription}
|
|
|
|
|
Problems: ${problems.map(p => p.problem).slice(0, 5).join(', ')}
|
|
|
|
|
|
|
|
|
|
Return JSON: {"personas": [{"name": "Descriptive name", "role": "Job title", "companySize": "e.g. 10-50 employees", "industry": "...", "painPoints": ["..."], "goals": ["..."], "techSavvy": "low|medium|high", "objections": ["..."], "searchBehavior": ["..."]}]}`
|
|
|
|
|
Return JSON: {"personas": [{"name": "Descriptive name", "role": "Job title", "companySize": "e.g. 10-50 employees", "industry": "...", "painPoints": ["..."], "goals": ["..."], "techSavvy": "low|medium|high", "objections": ["..."], "searchBehavior": ["..."]}]}
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ personas: Persona[] }>(prompt, systemPrompt, 0.5)
|
|
|
|
|
return result.personas
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function generateUseCases(features: Feature[], personas: Persona[], problems: Problem[]): Promise<UseCase[]> {
|
|
|
|
|
async function generateUseCases(
|
|
|
|
|
features: Feature[],
|
|
|
|
|
personas: Persona[],
|
|
|
|
|
problems: Problem[],
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<UseCase[]> {
|
|
|
|
|
const systemPrompt = `Create JTBD use case scenarios.`
|
|
|
|
|
const prompt = `Create 10 use cases.
|
|
|
|
|
Features: ${features.map(f => f.name).slice(0, 5).join(', ')}
|
|
|
|
|
Problems: ${problems.map(p => p.problem).slice(0, 3).join(', ')}
|
|
|
|
|
|
|
|
|
|
Return JSON: {"useCases": [{"scenario": "...", "trigger": "...", "emotionalState": "...", "currentWorkflow": ["..."], "desiredOutcome": "...", "alternativeProducts": ["..."], "whyThisProduct": "...", "churnRisk": ["..."]}]}`
|
|
|
|
|
Return JSON: {"useCases": [{"scenario": "...", "trigger": "...", "emotionalState": "...", "currentWorkflow": ["..."], "desiredOutcome": "...", "alternativeProducts": ["..."], "whyThisProduct": "...", "churnRisk": ["..."]}]}
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ useCases: UseCase[] }>(prompt, systemPrompt, 0.5)
|
|
|
|
|
return result.useCases
|
|
|
|
|
@@ -232,18 +374,128 @@ function generateDorkQueries(keywords: Keyword[], problems: Problem[], useCases:
|
|
|
|
|
return queries
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function generateDorkQueriesWithAI(
|
|
|
|
|
analysis: EnhancedProductAnalysis,
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<DorkQuery[]> {
|
|
|
|
|
const systemPrompt = `Generate high-signal search queries for forums. Return JSON only.`
|
|
|
|
|
const prompt = `Create 40-60 dork queries.
|
|
|
|
|
Product: ${analysis.productName}
|
|
|
|
|
Category: ${analysis.category}
|
|
|
|
|
Positioning: ${analysis.positioning}
|
|
|
|
|
Keywords: ${analysis.keywords.map(k => k.term).slice(0, 25).join(", ")}
|
|
|
|
|
Problems: ${analysis.problemsSolved.map(p => p.problem).slice(0, 10).join(", ")}
|
|
|
|
|
Competitors: ${analysis.competitors.map(c => c.name).slice(0, 10).join(", ")}
|
|
|
|
|
Use cases: ${analysis.useCases.map(u => u.scenario).slice(0, 8).join(", ")}
|
|
|
|
|
|
|
|
|
|
Rules:
|
|
|
|
|
- Use these platforms only: reddit, hackernews, indiehackers, quora, stackoverflow, twitter.
|
|
|
|
|
- Include intent: looking-for, frustrated, alternative, comparison, problem-solving, tutorial.
|
|
|
|
|
- Prefer query patterns like site:reddit.com "phrase" ...
|
|
|
|
|
|
|
|
|
|
Return JSON:
|
|
|
|
|
{"dorkQueries": [{"query": "...", "platform": "reddit|hackernews|indiehackers|twitter|quora|stackoverflow", "intent": "looking-for|frustrated|alternative|comparison|problem-solving|tutorial", "priority": "high|medium|low"}]}
|
|
|
|
|
|
|
|
|
|
${extraPrompt ? `User guidance: ${extraPrompt}` : ""}`
|
|
|
|
|
|
|
|
|
|
const result = await aiGenerate<{ dorkQueries: DorkQuery[] }>(prompt, systemPrompt, 0.3)
|
|
|
|
|
return result.dorkQueries
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AnalysisProgressUpdate = {
|
|
|
|
|
key: "features" | "competitors" | "keywords" | "problems" | "useCases" | "dorkQueries"
|
|
|
|
|
status: "running" | "completed"
|
|
|
|
|
detail?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function repromptSection(
|
|
|
|
|
sectionKey: "profile" | "features" | "competitors" | "keywords" | "problems" | "personas" | "useCases" | "dorkQueries",
|
|
|
|
|
content: ScrapedContent,
|
|
|
|
|
analysis: EnhancedProductAnalysis,
|
|
|
|
|
extraPrompt?: string
|
|
|
|
|
): Promise<any> {
|
|
|
|
|
if (sectionKey === "profile") {
|
|
|
|
|
const profile = await extractProductProfile(content, extraPrompt);
|
|
|
|
|
const tagline = content.metaDescription.split(".")[0];
|
|
|
|
|
const positioning = [
|
|
|
|
|
profile.primaryJTBD && profile.primaryJTBD !== "unknown"
|
|
|
|
|
? profile.primaryJTBD
|
|
|
|
|
: "",
|
|
|
|
|
profile.targetPersona && profile.targetPersona !== "unknown"
|
|
|
|
|
? `for ${profile.targetPersona}`
|
|
|
|
|
: "",
|
|
|
|
|
].filter(Boolean).join(" ");
|
|
|
|
|
return {
|
|
|
|
|
productName: profile.productName && profile.productName !== "unknown" ? profile.productName : analysis.productName,
|
|
|
|
|
tagline,
|
|
|
|
|
description: content.metaDescription,
|
|
|
|
|
category: profile.category && profile.category !== "unknown" ? profile.category : analysis.category,
|
|
|
|
|
positioning,
|
|
|
|
|
primaryJTBD: profile.primaryJTBD,
|
|
|
|
|
targetPersona: profile.targetPersona,
|
|
|
|
|
scopeBoundary: profile.scopeBoundary,
|
|
|
|
|
nonGoals: profile.nonGoals,
|
|
|
|
|
differentiators: profile.differentiators,
|
|
|
|
|
evidence: profile.evidence,
|
|
|
|
|
confidence: profile.confidence,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "features") {
|
|
|
|
|
return await extractFeatures(content, extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "competitors") {
|
|
|
|
|
const profile = await extractProductProfile(content, extraPrompt);
|
|
|
|
|
const candidateSet = await generateCompetitorCandidates(profile, extraPrompt);
|
|
|
|
|
return await selectDirectCompetitors(profile, candidateSet.candidates, extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "keywords") {
|
|
|
|
|
const features = analysis.features?.length ? analysis.features : await extractFeatures(content);
|
|
|
|
|
return await generateKeywords(features, content, analysis.competitors || [], extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "problems") {
|
|
|
|
|
const features = analysis.features?.length ? analysis.features : await extractFeatures(content);
|
|
|
|
|
return await identifyProblems(features, content, extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "personas") {
|
|
|
|
|
const problems = analysis.problemsSolved?.length
|
|
|
|
|
? analysis.problemsSolved
|
|
|
|
|
: await identifyProblems(analysis.features || [], content);
|
|
|
|
|
return await generatePersonas(content, problems, extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "useCases") {
|
|
|
|
|
const features = analysis.features?.length ? analysis.features : await extractFeatures(content);
|
|
|
|
|
const problems = analysis.problemsSolved?.length
|
|
|
|
|
? analysis.problemsSolved
|
|
|
|
|
: await identifyProblems(features, content);
|
|
|
|
|
const personas = analysis.personas?.length
|
|
|
|
|
? analysis.personas
|
|
|
|
|
: await generatePersonas(content, problems);
|
|
|
|
|
return await generateUseCases(features, personas, problems, extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sectionKey === "dorkQueries") {
|
|
|
|
|
return await generateDorkQueriesWithAI(analysis, extraPrompt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new Error(`Unsupported section key: ${sectionKey}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function performDeepAnalysis(
|
|
|
|
|
content: ScrapedContent,
|
|
|
|
|
onProgress?: (update: AnalysisProgressUpdate) => void | Promise<void>
|
|
|
|
|
): Promise<EnhancedProductAnalysis> {
|
|
|
|
|
console.log('🔍 Starting deep analysis...')
|
|
|
|
|
|
|
|
|
|
console.log(' 🧭 Product profiling...')
|
|
|
|
|
const productProfile = await extractProductProfile(content)
|
|
|
|
|
console.log(` ✓ Profiled as ${productProfile.category} for ${productProfile.targetPersona} (conf ${productProfile.confidence})`)
|
|
|
|
|
|
|
|
|
|
console.log(' 📦 Pass 1: Features...')
|
|
|
|
|
await onProgress?.({ key: "features", status: "running" })
|
|
|
|
|
const features = await extractFeatures(content)
|
|
|
|
|
@@ -252,7 +504,8 @@ export async function performDeepAnalysis(
|
|
|
|
|
|
|
|
|
|
console.log(' 🏆 Pass 2: Competitors...')
|
|
|
|
|
await onProgress?.({ key: "competitors", status: "running" })
|
|
|
|
|
const competitors = await identifyCompetitors(content)
|
|
|
|
|
const candidateSet = await generateCompetitorCandidates(productProfile)
|
|
|
|
|
const competitors = await selectDirectCompetitors(productProfile, candidateSet.candidates)
|
|
|
|
|
console.log(` ✓ ${competitors.length} competitors: ${competitors.map(c => c.name).join(', ')}`)
|
|
|
|
|
await onProgress?.({
|
|
|
|
|
key: "competitors",
|
|
|
|
|
@@ -295,13 +548,21 @@ export async function performDeepAnalysis(
|
|
|
|
|
|
|
|
|
|
const productName = content.title.split(/[\|\-–—:]/)[0].trim()
|
|
|
|
|
const tagline = content.metaDescription.split('.')[0]
|
|
|
|
|
const positioning = [
|
|
|
|
|
productProfile.primaryJTBD && productProfile.primaryJTBD !== "unknown"
|
|
|
|
|
? productProfile.primaryJTBD
|
|
|
|
|
: "",
|
|
|
|
|
productProfile.targetPersona && productProfile.targetPersona !== "unknown"
|
|
|
|
|
? `for ${productProfile.targetPersona}`
|
|
|
|
|
: "",
|
|
|
|
|
].filter(Boolean).join(" ")
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
productName,
|
|
|
|
|
productName: productProfile.productName && productProfile.productName !== "unknown" ? productProfile.productName : productName,
|
|
|
|
|
tagline,
|
|
|
|
|
description: content.metaDescription,
|
|
|
|
|
category: '',
|
|
|
|
|
positioning: '',
|
|
|
|
|
category: productProfile.category && productProfile.category !== "unknown" ? productProfile.category : '',
|
|
|
|
|
positioning,
|
|
|
|
|
features,
|
|
|
|
|
problemsSolved: problems,
|
|
|
|
|
personas,
|
|
|
|
|
@@ -310,6 +571,6 @@ export async function performDeepAnalysis(
|
|
|
|
|
competitors,
|
|
|
|
|
dorkQueries,
|
|
|
|
|
scrapedAt: new Date().toISOString(),
|
|
|
|
|
analysisVersion: '2.1-optimized'
|
|
|
|
|
analysisVersion: '2.2-profiled'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|