223 lines
9.4 KiB
TypeScript
223 lines
9.4 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { getAdminAuth, getAdminDb } from '@/lib/firebase/admin';
|
|
import { getAlloyDbClient } from '@/lib/db/alloydb';
|
|
import { GeminiLlmClient } from '@/lib/ai/gemini-client';
|
|
import { z } from 'zod';
|
|
|
|
const MissionFrameworkSchema = z.object({
|
|
targetCustomer: z.object({
|
|
primaryAudience: z.string().describe('Primary narrow target segment (include geography/region if mentioned in context)'),
|
|
theirSituation: z.string().describe('What situation or context they are in'),
|
|
relatedMarkets: z.array(z.string()).describe('2-4 additional related market segments or customer types that could benefit'),
|
|
}),
|
|
existingSolutions: z.array(z.object({
|
|
category: z.string().describe('Category of solution (e.g., "Legacy EMR Systems", "AI Scribes", "Practice Management", "Open Source")'),
|
|
description: z.string().describe('Description of this category and its limitations'),
|
|
products: z.array(z.object({
|
|
name: z.string().describe('Product/company name'),
|
|
url: z.string().optional().describe('Website URL if known'),
|
|
})).min(5).max(20).describe('Comprehensive list of 5-20 specific products in this category. Include all major players and notable solutions.'),
|
|
})).min(4).max(7).describe('4-7 categories of existing solutions with comprehensive product lists. ALWAYS include an "Open Source" category if applicable to the market.'),
|
|
innovations: z.array(z.object({
|
|
title: z.string().describe('Short title for this innovation (3-5 words)'),
|
|
description: z.string().describe('How this makes you different and better'),
|
|
})).describe('3 key innovations or differentiators'),
|
|
ideaValidation: z.array(z.object({
|
|
title: z.string().describe('Name of this validation metric'),
|
|
description: z.string().describe('What success looks like for this metric'),
|
|
})).describe('3 ways to validate the idea is sound'),
|
|
financialSuccess: z.object({
|
|
subscribers: z.number().describe('Target number of subscribers (Year 1)'),
|
|
pricePoint: z.number().describe('Monthly price per subscriber in dollars'),
|
|
retentionRate: z.number().describe('Target monthly retention rate as a percentage (0-100)'),
|
|
}),
|
|
});
|
|
|
|
export async function POST(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ projectId: string }> }
|
|
) {
|
|
try {
|
|
const { projectId } = await params;
|
|
|
|
// Authentication (skip in development if no auth header)
|
|
const authHeader = request.headers.get('Authorization');
|
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
|
|
if (!isDevelopment || authHeader?.startsWith('Bearer ')) {
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
const auth = getAdminAuth();
|
|
const decoded = await auth.verifyIdToken(token);
|
|
|
|
if (!decoded?.uid) {
|
|
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
|
}
|
|
}
|
|
|
|
console.log('[API /mission/generate] Generating mission framework for project:', projectId);
|
|
|
|
// Fetch insights from AlloyDB
|
|
let insights: any[] = [];
|
|
try {
|
|
const pool = await getAlloyDbClient();
|
|
const result = await pool.query(
|
|
`SELECT content, source_type, importance, created_at
|
|
FROM knowledge_chunks
|
|
WHERE project_id = $1
|
|
ORDER BY importance DESC, created_at DESC
|
|
LIMIT 50`,
|
|
[projectId]
|
|
);
|
|
insights = result.rows;
|
|
console.log('[API /mission/generate] Found', insights.length, 'insights');
|
|
} catch (dbError) {
|
|
console.log('[API /mission/generate] No AlloyDB insights available');
|
|
}
|
|
|
|
// Fetch knowledge items from Firestore
|
|
let knowledgeItems: any[] = [];
|
|
try {
|
|
const adminDb = getAdminDb();
|
|
const knowledgeSnapshot = await adminDb
|
|
.collection('knowledge')
|
|
.where('projectId', '==', projectId)
|
|
.orderBy('createdAt', 'desc')
|
|
.limit(20)
|
|
.get();
|
|
|
|
knowledgeItems = knowledgeSnapshot.docs.map(doc => doc.data());
|
|
console.log('[API /mission/generate] Found', knowledgeItems.length, 'knowledge items');
|
|
} catch (firestoreError) {
|
|
console.log('[API /mission/generate] No Firestore knowledge available');
|
|
}
|
|
|
|
// Get project data
|
|
let projectData: any = {};
|
|
try {
|
|
const adminDb = getAdminDb();
|
|
const projectDoc = await adminDb.collection('projects').doc(projectId).get();
|
|
if (projectDoc.exists) {
|
|
projectData = projectDoc.data();
|
|
}
|
|
} catch (error) {
|
|
console.log('[API /mission/generate] Could not fetch project data');
|
|
}
|
|
|
|
// Build context from available data
|
|
const contextParts = [];
|
|
|
|
if (projectData?.productVision) {
|
|
contextParts.push(`Product Vision: ${projectData.productVision}`);
|
|
}
|
|
|
|
if (projectData?.phaseData?.canonicalProductModel) {
|
|
const model = projectData.phaseData.canonicalProductModel;
|
|
if (model.oneLiner) contextParts.push(`Product Description: ${model.oneLiner}`);
|
|
if (model.problem) contextParts.push(`Problem: ${model.problem}`);
|
|
if (model.targetUser) contextParts.push(`Target User: ${model.targetUser}`);
|
|
if (model.coreSolution) contextParts.push(`Solution: ${model.coreSolution}`);
|
|
}
|
|
|
|
if (insights.length > 0) {
|
|
const insightTexts = insights.slice(0, 10).map(i => i.content).join('\n- ');
|
|
contextParts.push(`Key Insights:\n- ${insightTexts}`);
|
|
}
|
|
|
|
if (knowledgeItems.length > 0) {
|
|
const knowledgeTexts = knowledgeItems.slice(0, 5)
|
|
.map(k => k.title || k.content?.substring(0, 100))
|
|
.filter(Boolean)
|
|
.join('\n- ');
|
|
if (knowledgeTexts) {
|
|
contextParts.push(`Additional Context:\n- ${knowledgeTexts}`);
|
|
}
|
|
}
|
|
|
|
const context = contextParts.length > 0
|
|
? contextParts.join('\n\n')
|
|
: 'No project context available yet. Please create a generic framework based on best practices for new product development.';
|
|
|
|
console.log('[API /mission/generate] Context length:', context.length);
|
|
|
|
// Use AI to generate the mission framework
|
|
const llm = new GeminiLlmClient();
|
|
const systemPrompt = `You are a product strategy expert. Based on the provided project information, create a comprehensive mission framework that helps the founder clearly articulate their product vision, market position, and success metrics.
|
|
|
|
CRITICAL: For Target Customer, be VERY SPECIFIC and NARROW:
|
|
- Look for geographic/regional targeting in the context (country, state, city, region)
|
|
- Look for specific customer segments, verticals, or niches
|
|
- Avoid broad generalizations like "all doctors" or "businesses everywhere"
|
|
- If region is mentioned, ALWAYS include it in the primary audience
|
|
- Target the smallest viable market segment that can sustain the business
|
|
|
|
Be specific and actionable. Use the project context to inform your recommendations.`;
|
|
|
|
const userPrompt = `Based on this project information, generate a complete mission framework:
|
|
|
|
${context}
|
|
|
|
Create a structured mission framework that includes:
|
|
|
|
1. Target Customer:
|
|
- Primary Audience: Be EXTREMELY SPECIFIC and narrow (include geography if mentioned)
|
|
Example: "Solo family practice physicians in rural Oregon" NOT "Primary care doctors"
|
|
- Their Situation: What problem/context they face
|
|
- Related Markets: List 2-4 other related customer segments that could also benefit
|
|
Example: ["Urgent care clinics", "Pediatric specialists in small practices", "Telemedicine providers"]
|
|
|
|
2. Existing Solutions: Group into 4-7 CATEGORIES (e.g., "Legacy EMR Systems", "AI Medical Scribes", "Open Source", etc.)
|
|
- For each category: provide a description of what they do and their limitations
|
|
- List 5-20 specific PRODUCTS/COMPANIES in each category with website URLs if you know them
|
|
- Be COMPREHENSIVE - include all major players, notable solutions, and emerging alternatives
|
|
- ALWAYS include an "Open Source" category listing relevant open-source alternatives (GitHub, frameworks, libraries, tools)
|
|
- Include direct competitors, adjacent solutions, and legacy approaches
|
|
|
|
3. Your Innovations (3 key differentiators)
|
|
4. Idea Validation (3 validation metrics)
|
|
5. Financial Success (subscribers, price point, retention rate)
|
|
|
|
Be comprehensive with existing solutions. Be specific and narrow with primary target, but show the range of related markets.`;
|
|
|
|
const result = await llm.structuredCall({
|
|
model: 'gemini',
|
|
systemPrompt,
|
|
messages: [{ role: 'user', content: userPrompt }],
|
|
schema: MissionFrameworkSchema,
|
|
temperature: 0.7,
|
|
});
|
|
|
|
console.log('[API /mission/generate] Successfully generated mission framework');
|
|
|
|
// Store the generated framework in Firestore
|
|
try {
|
|
const adminDb = getAdminDb();
|
|
await adminDb.collection('projects').doc(projectId).update({
|
|
'phaseData.missionFramework': result,
|
|
'phaseData.missionFrameworkUpdatedAt': new Date(),
|
|
});
|
|
console.log('[API /mission/generate] Saved framework to Firestore');
|
|
} catch (saveError) {
|
|
console.error('[API /mission/generate] Could not save framework:', saveError);
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
framework: result,
|
|
});
|
|
} catch (error) {
|
|
console.error('[API /mission/generate] Error:', error);
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Failed to generate mission framework',
|
|
details: error instanceof Error ? error.message : String(error)
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|