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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions app/api/ai/generate-invoice/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
48 changes: 47 additions & 1 deletion components/invoices/invoice-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }))
}
Expand All @@ -66,8 +96,24 @@ export function InvoiceForm() {
</div>

<div>
<label className="block text-sm font-medium text-brand-black mb-2">Description *</label>
<div className="flex justify-between items-center mb-2">
<label className="block text-sm font-medium text-brand-black">Description *</label>
<button
type="button"
onClick={handleMagicWrite}
disabled={isGenerating || !form.description.trim()}
className="flex items-center gap-1.5 text-xs font-semibold text-brand-black bg-brand-light px-3 py-1.5 rounded-full hover:bg-gray-200 transition-colors disabled:opacity-50"
>
{isGenerating ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Wand2 className="w-3.5 h-3.5" />
)}
Magic Write
</button>
</div>
<textarea name="description" value={form.description} onChange={handleChange} required rows={3} className="w-full px-4 py-3 rounded-lg border border-brand-border focus:border-brand-black outline-none resize-none" placeholder="Logo design, website development, etc." />
<p className="mt-1 text-xs text-gray-500 italic">Enter keywords and click 'Magic Write' for a professional touch.</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
Expand Down
21 changes: 17 additions & 4 deletions lib/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
/**
* Simple logging utility for error reporting.
* In a production environment, this should be integrated with services like Sentry, LogRocket, or Datadog.
*/
import pino from 'pino';

export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development' ? {
target: 'pino-pretty',
options: {
colorize: true,
},
} : undefined,
base: {
env: process.env.NODE_ENV,
revision: process.env.VERCEL_GIT_COMMIT_SHA,
},
});



export const logError = (error: Error, errorInfo?: { [key: string]: any }) => {
// Always log to console in development
Expand Down
118 changes: 108 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading