117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { query, getConfig } from '@/lib/db';
|
|
import { generate } from '@/lib/ai/client';
|
|
import { prompts } from '@/lib/ai/prompts';
|
|
import { ToolName, Session } from '@/lib/types';
|
|
|
|
const validTools: ToolName[] = ['logframe', 'funder-rewriter', 'smart-indicators', 'theory-of-change'];
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: { params: { tool: string } }
|
|
) {
|
|
try {
|
|
const tool = params.tool as ToolName;
|
|
if (!validTools.includes(tool)) {
|
|
return NextResponse.json({ error: 'Invalid tool' }, { status: 400 });
|
|
}
|
|
|
|
const body = await request.json();
|
|
const { input, email } = body as { input: Record<string, string>; email?: string };
|
|
|
|
if (!input) {
|
|
return NextResponse.json({ error: 'Input required' }, { status: 400 });
|
|
}
|
|
|
|
// --- Free tier check ---
|
|
// Email is required to use the free tier. If no email, require payment immediately.
|
|
let isFreeTier = false;
|
|
if (email) {
|
|
const freeTierEnabled = (await getConfig('free_tier_enabled')) !== 'false';
|
|
if (freeTierEnabled) {
|
|
// Check if this email has ever generated before (any tool)
|
|
const existing = await query<{ id: number }>(
|
|
'SELECT id FROM leads WHERE email = $1 LIMIT 1',
|
|
[email]
|
|
);
|
|
if (existing.length === 0) {
|
|
// First time — insert lead, grant free tier
|
|
await query(
|
|
`INSERT INTO leads (email, org_name, tool) VALUES ($1, $2, $3)
|
|
ON CONFLICT (email) DO NOTHING`,
|
|
[email, input.organisation || input.project_title || null, tool]
|
|
);
|
|
isFreeTier = true;
|
|
}
|
|
// If email already exists → not free, falls through to paid path below
|
|
}
|
|
}
|
|
|
|
// Create session — track whether it starts as paid (free tier unlock)
|
|
const sessions = await query<Session>(
|
|
`INSERT INTO sessions (tool, email, input, is_paid) VALUES ($1, $2, $3, $4) RETURNING *`,
|
|
[tool, email || null, JSON.stringify(input), isFreeTier]
|
|
);
|
|
const session = sessions[0];
|
|
|
|
// Generate the draft
|
|
const toolPrompts = prompts[tool];
|
|
const systemPrompt = `You are a seasoned international development consultant with 20+ years of experience designing and evaluating programmes for major donors including USAID, DFID/FCDO, the EU, UN agencies, the World Bank, and large private foundations. You have written and reviewed hundreds of successful grant applications, logframes, theories of change, and M&E frameworks across sub-Saharan Africa, South and Southeast Asia, the Middle East, and Latin America.
|
|
|
|
Your writing is:
|
|
- Precise, evidence-informed, and donor-ready
|
|
- Grounded in current best practice (adaptive management, systems thinking, feminist M&E, localisation agendas)
|
|
- Free of jargon and buzzwords unless they carry specific technical meaning
|
|
- Structured clearly with logical hierarchy and consistent formatting
|
|
- Calibrated to the specific sector, geography, and donor context provided
|
|
|
|
Always produce outputs that a programme officer could submit with minimal revision. Favour concrete, measurable language over vague aspirations.`;
|
|
const userPrompt = toolPrompts.master(input);
|
|
|
|
const result = await generate(systemPrompt, userPrompt);
|
|
|
|
// Store full output in session
|
|
await query(
|
|
`UPDATE sessions SET output = $1, updated_at = NOW() WHERE id = $2`,
|
|
[result.text, session.id]
|
|
);
|
|
|
|
// Log usage
|
|
await query(
|
|
`INSERT INTO usage (session_id, tool, action, tokens_used) VALUES ($1, $2, $3, $4)`,
|
|
[session.id, tool, 'generate', result.tokens]
|
|
);
|
|
|
|
// Preview = first ~40% of content (chars)
|
|
const previewEnd = Math.floor(result.text.length * 0.40);
|
|
const preview = result.text.substring(0, previewEnd);
|
|
|
|
// If free tier or already paid → return full output
|
|
if (isFreeTier) {
|
|
return NextResponse.json({
|
|
session_id: session.id,
|
|
preview,
|
|
full_output: result.text,
|
|
is_paid: true,
|
|
free_tier_used: true,
|
|
});
|
|
}
|
|
|
|
// Otherwise return preview only — requires payment to unlock
|
|
return NextResponse.json({
|
|
session_id: session.id,
|
|
preview,
|
|
full_length: result.text.length,
|
|
is_paid: false,
|
|
requires_email: !email, // hint to frontend to prompt for email
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Generate draft error:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to generate draft' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|