diff --git a/.env.example b/.env.example index ca8dca9..eaa801f 100644 --- a/.env.example +++ b/.env.example @@ -87,6 +87,11 @@ BADGE_ISSUER_SECRET_KEY=SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX COINGECKO_API_URL="https://api.coingecko.com/api/v3" + +# ---------- AI Settings (backend/server-only) ---------- +# API key for OpenAI (used for generating invoice descriptions) +OPENAI_API_KEY=sk-proj-xxxxx +======= # ---------- KYC Rate Limiting ---------- # Comma-separated list of internal user IDs that bypass KYC rate limits. # Used for admin accounts and automated testing pipelines. diff --git a/app/api/ai/generate-invoice/route.ts b/app/api/ai/generate-invoice/route.ts new file mode 100644 index 0000000..4bf63b1 --- /dev/null +++ b/app/api/ai/generate-invoice/route.ts @@ -0,0 +1,34 @@ +import { openai } from '@ai-sdk/openai'; +import { generateText } from 'ai'; +import { NextRequest, NextResponse } from 'next/server'; +import { logger } from '@/lib/logger'; + +export async function POST(req: NextRequest) { + try { + const { keywords } = await req.json(); + + if (!keywords || typeof keywords !== 'string') { + return NextResponse.json({ error: 'Keywords are required' }, { status: 400 }); + } + + logger.info({ keywords }, 'Generating invoice description'); + + const { text } = await generateText({ + model: openai('gpt-4o'), + system: `You are a professional business assistant. + Your task is to convert simple keywords into a professional, clear, and itemized invoice description. + The tone should be formal and suitable for a freelance or business invoice. + Format the output as a concise paragraph or a clear list if multiple items are detected. + Avoid fluff and focus on clarity.`, + prompt: `Translate these keywords into a professional invoice description: ${keywords}`, + }); + + return NextResponse.json({ description: text }); + } catch (error) { + logger.error({ err: error }, 'Failed to generate invoice description'); + return NextResponse.json( + { error: 'Failed to generate description' }, + { status: 500 } + ); + } +} diff --git a/components/invoices/invoice-form.tsx b/components/invoices/invoice-form.tsx index 169438d..ae49d4a 100644 --- a/components/invoices/invoice-form.tsx +++ b/components/invoices/invoice-form.tsx @@ -3,11 +3,14 @@ import { useState } from 'react' import { useRouter } from 'next/navigation' import { usePrivy } from '@privy-io/react-auth' +import { Wand2, Loader2 } from 'lucide-react' +import { logger } from '@/lib/logger' export function InvoiceForm() { const router = useRouter() const { getAccessToken } = usePrivy() const [isLoading, setIsLoading] = useState(false) + const [isGenerating, setIsGenerating] = useState(false) const [error, setError] = useState('') const [form, setForm] = useState({ clientEmail: '', clientName: '', description: '', amount: '', currency: 'USD', dueDate: '' }) const [isRecurring, setIsRecurring] = useState(false) @@ -46,6 +49,33 @@ export function InvoiceForm() { } } + const handleMagicWrite = async () => { + if (!form.description.trim()) { + setError('Please enter some keywords first') + return + } + + setIsGenerating(true) + setError('') + + try { + const res = await fetch('/api/ai/generate-invoice', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ keywords: form.description }), + }) + + if (!res.ok) throw new Error('Failed to generate description') + const data = await res.json() + setForm(prev => ({ ...prev, description: data.description })) + } catch (err) { + logger.error({ err }, 'Magic Write failed') + setError('Failed to generate professional description') + } finally { + setIsGenerating(false) + } + } + const handleChange = (e: React.ChangeEvent) => { setForm(prev => ({ ...prev, [e.target.name]: e.target.value })) } @@ -66,8 +96,24 @@ export function InvoiceForm() {
- +
+ + +