Initial commit

This commit is contained in:
2026-03-18 18:26:24 +08:00
commit c784d07796
60 changed files with 3756 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
'use client';
interface BlurOverlayProps {
onUnlock: () => void;
loading?: boolean;
}
export default function BlurOverlay({ onUnlock, loading }: BlurOverlayProps) {
return (
<div className="absolute inset-0 z-10 flex items-center justify-center backdrop-blur-md bg-white/60 rounded-lg">
<div className="text-center p-8 max-w-md">
<div className="text-4xl mb-4">🔒</div>
<h3 className="text-xl font-bold text-dark mb-2">Unlock Full Output</h3>
<p className="text-gray-600 mb-6">
Your complete, professional-quality output is ready. Unlock it to access the full document, improvements, validation, and exports.
</p>
<button
onClick={onUnlock}
disabled={loading}
className="bg-accent hover:bg-accent/90 text-white px-8 py-3 rounded-lg font-semibold transition-colors disabled:opacity-50"
>
{loading ? 'Processing...' : 'Unlock Now — Pay Once'}
</button>
<p className="text-xs text-gray-400 mt-3">Secure payment via LemonSqueezy</p>
</div>
</div>
);
}
+120
View File
@@ -0,0 +1,120 @@
'use client';
import { useState } from 'react';
interface EditableOutputProps {
content: string;
sessionId: string;
tool: string;
isPaid: boolean;
improvementsUsed: number;
maxImprovements: number;
onImproved: (newContent: string) => void;
}
export default function EditableOutput({
content,
sessionId,
tool,
isPaid,
improvementsUsed,
maxImprovements,
onImproved,
}: EditableOutputProps) {
const [feedback, setFeedback] = useState('');
const [loading, setLoading] = useState(false);
const [validating, setValidating] = useState(false);
const [validation, setValidation] = useState<string | null>(null);
const handleImprove = async () => {
if (!feedback.trim()) return;
setLoading(true);
try {
const res = await fetch(`/api/${tool}/improve-section`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, section: content, feedback }),
});
const data = await res.json();
if (data.improved) {
onImproved(data.improved);
setFeedback('');
}
} catch (e) {
console.error('Improve failed:', e);
} finally {
setLoading(false);
}
};
const handleValidate = async () => {
setValidating(true);
try {
const res = await fetch(`/api/${tool}/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId }),
});
const data = await res.json();
if (data.validation) setValidation(data.validation);
} catch (e) {
console.error('Validate failed:', e);
} finally {
setValidating(false);
}
};
const remaining = maxImprovements - improvementsUsed;
return (
<div className="space-y-4">
<div className="prose prose-sm max-w-none bg-gray-50 p-6 rounded-lg border whitespace-pre-wrap font-mono text-sm">
{content}
</div>
{isPaid && (
<>
<div className="flex gap-2">
<button
onClick={handleValidate}
disabled={validating}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 disabled:opacity-50"
>
{validating ? 'Analysing...' : '🔍 Analyse Quality'}
</button>
<span className="text-xs text-gray-400 self-center">
{remaining} improvements remaining
</span>
</div>
{validation && (
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg">
<h4 className="font-semibold text-blue-800 mb-2">Quality Assessment</h4>
<div className="text-sm whitespace-pre-wrap">{validation}</div>
</div>
)}
{remaining > 0 && (
<div className="flex gap-2">
<input
type="text"
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="Describe what to improve..."
className="flex-1 border rounded-lg px-3 py-2 text-sm"
onKeyDown={(e) => e.key === 'Enter' && handleImprove()}
/>
<button
onClick={handleImprove}
disabled={loading || !feedback.trim()}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent/90 disabled:opacity-50"
>
{loading ? 'Improving...' : '✨ Improve'}
</button>
</div>
)}
</>
)}
</div>
);
}
+80
View File
@@ -0,0 +1,80 @@
'use client';
import { useState } from 'react';
interface ExportMenuProps {
sessionId: string;
disabled?: boolean;
}
export default function ExportMenu({ sessionId, disabled }: ExportMenuProps) {
const [open, setOpen] = useState(false);
const [exporting, setExporting] = useState<string | null>(null);
const handleExport = async (format: string) => {
setExporting(format);
try {
if (format === 'copy') {
const res = await fetch(`/api/export/copy`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId }),
});
const data = await res.json();
await navigator.clipboard.writeText(data.text);
alert('Copied to clipboard!');
} else {
const res = await fetch(`/api/export/${format}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId }),
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `output.${format === 'excel' ? 'xlsx' : format}`;
a.click();
URL.revokeObjectURL(url);
}
} catch (e) {
console.error('Export failed:', e);
} finally {
setExporting(null);
setOpen(false);
}
};
const formats = [
{ key: 'copy', label: '📋 Copy to Clipboard', ext: '' },
{ key: 'docx', label: '📄 Word Document', ext: '.docx' },
{ key: 'excel', label: '📊 Excel Spreadsheet', ext: '.xlsx' },
{ key: 'pdf', label: '📕 PDF Document', ext: '.pdf' },
];
return (
<div className="relative">
<button
onClick={() => setOpen(!open)}
disabled={disabled}
className="px-4 py-2 bg-dark text-white rounded-lg text-sm hover:bg-dark/90 disabled:opacity-50"
>
📥 Export
</button>
{open && (
<div className="absolute right-0 top-full mt-1 bg-white border rounded-lg shadow-lg py-1 w-56 z-20">
{formats.map((f) => (
<button
key={f.key}
onClick={() => handleExport(f.key)}
disabled={exporting !== null}
className="w-full text-left px-4 py-2 hover:bg-gray-50 text-sm disabled:opacity-50"
>
{exporting === f.key ? 'Exporting...' : f.label}
</button>
))}
</div>
)}
</div>
);
}
+124
View File
@@ -0,0 +1,124 @@
'use client';
import { useState } from 'react';
import BlurOverlay from './BlurOverlay';
import EditableOutput from './EditableOutput';
import ExportMenu from './ExportMenu';
type State = 'idle' | 'generating' | 'preview' | 'unlocked' | 'done';
interface OutputPanelProps {
tool: string;
sessionId: string | null;
preview: string | null;
fullOutput: string | null;
isPaid: boolean;
onPaymentRequest: () => void;
paymentLoading?: boolean;
}
export default function OutputPanel({
tool,
sessionId,
preview,
fullOutput,
isPaid,
onPaymentRequest,
paymentLoading,
}: OutputPanelProps) {
const [output, setOutput] = useState(fullOutput);
const [finalizing, setFinalizing] = useState(false);
const state: State = !sessionId
? 'idle'
: !preview
? 'generating'
: !isPaid
? 'preview'
: finalizing
? 'done'
: 'unlocked';
const displayOutput = output || fullOutput;
const handleFinalize = async () => {
setFinalizing(true);
try {
const res = await fetch(`/api/${tool}/finalize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId }),
});
const data = await res.json();
if (data.finalized) setOutput(data.finalized);
} catch (e) {
console.error('Finalize failed:', e);
}
};
if (state === 'idle') {
return (
<div className="h-full flex items-center justify-center text-gray-400">
<div className="text-center">
<div className="text-5xl mb-4">📝</div>
<p>Fill in the form and click Generate to get started</p>
</div>
</div>
);
}
if (state === 'generating') {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="animate-spin text-4xl mb-4"></div>
<p className="text-gray-600 font-medium">Generating your output...</p>
<p className="text-gray-400 text-sm mt-1">This may take 15-30 seconds</p>
</div>
</div>
);
}
return (
<div className="relative">
{state === 'preview' && (
<>
<div className="prose prose-sm max-w-none bg-gray-50 p-6 rounded-lg border whitespace-pre-wrap font-mono text-sm">
{preview}
<span className="text-gray-300">{'...'.repeat(20)}</span>
</div>
<BlurOverlay onUnlock={onPaymentRequest} loading={paymentLoading} />
</>
)}
{(state === 'unlocked' || state === 'done') && sessionId && displayOutput && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-dark">Your Output</h3>
<div className="flex gap-2">
{!finalizing && (
<button
onClick={handleFinalize}
className="px-4 py-2 bg-accent text-white rounded-lg text-sm hover:bg-accent/90"
>
🎯 Finalize
</button>
)}
<ExportMenu sessionId={sessionId} />
</div>
</div>
<EditableOutput
content={displayOutput}
sessionId={sessionId}
tool={tool}
isPaid={true}
improvementsUsed={0}
maxImprovements={5}
onImproved={setOutput}
/>
</div>
)}
</div>
);
}
+70
View File
@@ -0,0 +1,70 @@
'use client';
import { useState } from 'react';
import { FormField } from '@/lib/types';
interface ToolFormProps {
fields: FormField[];
onSubmit: (data: Record<string, string>) => void;
loading?: boolean;
}
export default function ToolForm({ fields, onSubmit, loading }: ToolFormProps) {
const [values, setValues] = useState<Record<string, string>>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(values);
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{fields.map((field) => (
<div key={field.name}>
<label className="block text-sm font-medium text-dark mb-1">
{field.label}
{field.required && <span className="text-red-500 ml-1">*</span>}
</label>
{field.type === 'textarea' ? (
<textarea
value={values[field.name] || ''}
onChange={(e) => setValues({ ...values, [field.name]: e.target.value })}
placeholder={field.placeholder}
required={field.required}
rows={4}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-accent/50 focus:border-accent"
/>
) : field.type === 'select' ? (
<select
value={values[field.name] || ''}
onChange={(e) => setValues({ ...values, [field.name]: e.target.value })}
required={field.required}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-accent/50 focus:border-accent"
>
<option value="">Select...</option>
{field.options?.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
) : (
<input
type="text"
value={values[field.name] || ''}
onChange={(e) => setValues({ ...values, [field.name]: e.target.value })}
placeholder={field.placeholder}
required={field.required}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-accent/50 focus:border-accent"
/>
)}
</div>
))}
<button
type="submit"
disabled={loading}
className="w-full bg-accent hover:bg-accent/90 text-white py-3 rounded-lg font-semibold transition-colors disabled:opacity-50"
>
{loading ? 'Generating...' : '✨ Generate'}
</button>
</form>
);
}