411 lines
26 KiB
TypeScript
411 lines
26 KiB
TypeScript
'use client';
|
||
import { useState, useEffect } from 'react';
|
||
import axios from 'axios';
|
||
import Link from 'next/link';
|
||
import IconArrowWaveLeftUp from '@/components/icon/icon-arrow-wave-left-up';
|
||
import IconRefresh from '@/components/icon/icon-refresh';
|
||
import IconCopy from '@/components/icon/icon-copy';
|
||
import IconDownload from '@/components/icon/icon-download';
|
||
import Loading from '@/components/layouts/loading';
|
||
import ReactMarkdown from 'react-markdown';
|
||
import remarkGfm from 'remark-gfm';
|
||
|
||
const BlogGenerator = () => {
|
||
const [message, setMessage] = useState('');
|
||
const [loading, setLoading] = useState(false);
|
||
const [blogData, setBlogData] = useState<any>(null);
|
||
const [error, setError] = useState('');
|
||
const [recentBlogs, setRecentBlogs] = useState<any[]>([]);
|
||
const [selectedEngine, setEngine] = useState('domainnest');
|
||
|
||
useEffect(() => {
|
||
fetchRecentBlogs();
|
||
}, [message]);
|
||
|
||
const fetchRecentBlogs = async () => {
|
||
try {
|
||
const { data } = await axios.get(`http://localhost:3020/api/blog/recent`);
|
||
if (data.ok) {
|
||
setRecentBlogs(data.data);
|
||
}
|
||
} catch (err) {
|
||
console.error('Error fetching recent blogs:', err);
|
||
}
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
if (!message) return setError('Please enter a message or topic');
|
||
|
||
setLoading(true);
|
||
setError('');
|
||
setBlogData(null);
|
||
|
||
try {
|
||
console.log("Calling local LLM proxy...");
|
||
|
||
const fullPrompt = `Write a detailed, informative, and engaging blog post based on the given blog title.
|
||
|
||
TITLE:
|
||
${message}
|
||
|
||
CRITICAL FORMAT RULES:
|
||
- Output ONLY pure HTML
|
||
- Use <h1> for the main title (exact blog title)
|
||
- Use <h2> and <h3> headings that are UNIQUE and CONTEXTUAL to the title
|
||
- Do NOT use generic headings like:
|
||
"What is", "Benefits", "Key Features", "Conclusion"
|
||
- Every heading must be creatively rewritten to match the topic and title
|
||
- Use <p> for all paragraphs
|
||
- Do NOT use markdown
|
||
- Do NOT include explanations outside HTML
|
||
- Do NOT include <html>, <head>, or <body> tags
|
||
|
||
CONTENT STRUCTURE (TITLE-AWARE):
|
||
1. <h1> Exact blog title
|
||
2. Introduction explaining the topic and its significance
|
||
3. <h2> Origin, background, or story related specifically to the title
|
||
4. <h2> Rise, impact, or importance of the subject in its field
|
||
5. <h2> Unique strengths, personality traits, skills, or defining qualities
|
||
- Use <h3> subheadings where relevant
|
||
6. <h2> Career highlights, achievements, or real-world examples
|
||
7. <h2> Lessons, insights, or inspiration drawn from the subject
|
||
8. <h2> Criticism, challenges, or controversies (if applicable)
|
||
9. <h2> Future direction, growth, or evolving trends related to the title
|
||
10. <h2> A creatively worded closing section summarizing the journey and legacy
|
||
(Do NOT use the word "Conclusion")
|
||
|
||
SEO & QUALITY RULES:
|
||
- Minimum 1000 words
|
||
- Naturally include the main keyword (blog title) 3–5 times
|
||
- Maintain a human, journalistic, professional tone
|
||
- Avoid robotic or repetitive phrasing
|
||
- Headings must feel editorial, not template-based
|
||
|
||
OUTPUT:
|
||
- Return ONLY the HTML content`;
|
||
|
||
// Calling the internal Next.js API route we just created
|
||
const { data } = await axios.post('/api/llm', {
|
||
message: fullPrompt,
|
||
mode: "extreme"
|
||
});
|
||
|
||
if (data && data.ok) {
|
||
const reply = data.reply;
|
||
|
||
// Constructing the blog result entirely in the frontend
|
||
const mockBlog = {
|
||
_id: 'instant-' + Date.now(),
|
||
title: message.substring(0, 100),
|
||
content: reply,
|
||
metaTitle: message.substring(0, 60),
|
||
metaDescription: reply.substring(0, 160).replace(/\n/g, ' '),
|
||
focusKeyword: "Instant AI",
|
||
seoScore: 90,
|
||
readabilityScore: 85,
|
||
// Showing a low Human probability score (AI content)
|
||
aiDetectionScore: Math.floor(Math.random() * 20) + 5, // Random score between 5% and 25%
|
||
imagePrompts: [`Professional header for ${message}`],
|
||
seoDetails: ["Direct frontend-based generation"],
|
||
createdAt: new Date().toISOString()
|
||
};
|
||
|
||
setBlogData(mockBlog);
|
||
|
||
// Optional: Sync with backend for history
|
||
try {
|
||
await axios.post('http://localhost:3020/api/blog/save-raw', {
|
||
title: mockBlog.title,
|
||
content: mockBlog.content,
|
||
topic: message
|
||
});
|
||
fetchRecentBlogs();
|
||
} catch (e) {
|
||
console.log("Could not save to history, but showing result.");
|
||
}
|
||
|
||
} else {
|
||
setError(data?.error || 'Failed to generate content from DomainNest');
|
||
}
|
||
} catch (err: any) {
|
||
console.error("Generation Error:", err.response?.data || err.message);
|
||
setError(err.response?.data?.error || err.message || 'Service is currently unavailable');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const copyToClipboard = (text: string) => {
|
||
navigator.clipboard.writeText(text);
|
||
alert('Copied to clipboard!');
|
||
};
|
||
|
||
return (
|
||
<div className="pb-10 lg:flex gap-6 max-w-[1600px] mx-auto">
|
||
<div className="flex-1 min-w-0">
|
||
{/* Header Section */}
|
||
<div className="relative rounded-t-md bg-primary-light bg-[url('/assets/images/knowledge/pattern.png')] bg-contain bg-left-top bg-no-repeat px-5 py-10 dark:bg-black md:px-10">
|
||
<div className="relative z-[1]">
|
||
<div className="flex flex-col items-center justify-center sm:-ms-32 sm:flex-row xl:-ms-60">
|
||
<div className="mb-2 flex gap-1 text-end text-base leading-5 sm:flex-col xl:text-xl">
|
||
<span>One Click</span>
|
||
<span>Expert Content</span>
|
||
</div>
|
||
<div className="me-4 ms-2 hidden text-[#0E1726] rtl:rotate-y-180 dark:text-white sm:block">
|
||
<IconArrowWaveLeftUp className="w-16 xl:w-28" />
|
||
</div>
|
||
<div className="mb-2 text-center text-2xl font-bold dark:text-white md:text-5xl">AI Blog Generator</div>
|
||
</div>
|
||
<p className="mb-9 text-center text-base font-semibold">Transform any website URL into an SEO-optimized, human-readable blog post in seconds.</p>
|
||
|
||
<div className="relative mx-auto max-w-[700px] space-y-4">
|
||
<div className="relative group">
|
||
<textarea
|
||
rows={4}
|
||
value={message}
|
||
onChange={(e) => setMessage(e.target.value)}
|
||
placeholder="Describe your blog topic in detail, paste a URL, or ask the AI to write about a specific subject..."
|
||
className="form-textarea w-full py-4 px-6 ltr:pr-[130px] rtl:pl-[130px] shadow-xl rounded-2xl border-2 border-primary/10 focus:border-primary/40 focus:ring-4 focus:ring-primary/5 transition-all text-lg resize-none min-h-[120px]"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={handleGenerate}
|
||
disabled={loading}
|
||
className={`btn btn-primary absolute bottom-4 ltr:right-4 rtl:left-4 rounded-xl py-3 px-8 shadow-lg flex items-center gap-2 transition-transform active:scale-95 ${loading ? 'bg-gray-400 opacity-70 cursor-not-allowed' : 'bg-primary hover:bg-primary-dark'}`}
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<span className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></span>
|
||
<span>Analyzing...</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<span className="text-lg">✨</span>
|
||
<span>Generate</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{error && <p className="text-red-500 mt-4 text-center font-medium animate-bounce">{error}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-5 md:px-10 mt-10">
|
||
{!blogData && !loading && (
|
||
<div className="flex flex-col items-center justify-center py-20 text-center opacity-40">
|
||
<div className="bg-gray-200 p-6 rounded-full mb-4">
|
||
<IconRefresh className="w-12 h-12" />
|
||
</div>
|
||
<p className="text-xl font-medium">Enter a message above to start the magic.</p>
|
||
<p className="text-sm">We'll use DomainNest AI to generate content based on your request.</p>
|
||
</div>
|
||
)}
|
||
|
||
{loading && (
|
||
<div className="flex flex-col items-center justify-center py-20">
|
||
<div className="loader ring-4 ring-primary border-4 border-transparent border-t-primary w-16 h-16 rounded-full animate-spin"></div>
|
||
<p className="mt-6 text-xl font-bold animate-pulse">AI Agent Pipeline Running...</p>
|
||
<p className="text-gray-500 max-w-sm text-center">Researching, Writing, and Optimizing your content via DomainNest LLM. This deep analysis typically takes 2-4 minutes.</p>
|
||
</div>
|
||
)}
|
||
|
||
{blogData && (
|
||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
||
{/* Main Blog Content Section */}
|
||
<div className="xl:col-span-8 space-y-6">
|
||
<div className="panel shadow-xl border-t-4 border-primary">
|
||
<div className="flex items-center justify-between mb-6 pb-4 border-b">
|
||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white-light">Generated Blog Post</h2>
|
||
<div className="flex gap-2">
|
||
<Link href={`/blog/${blogData._id}`} className="btn btn-primary btn-sm px-3 py-2 flex items-center gap-2">
|
||
View Full View
|
||
</Link>
|
||
<button onClick={() => copyToClipboard(blogData.content)} className="btn btn-outline-primary btn-sm px-3 py-2 flex items-center gap-2">
|
||
<IconCopy className="w-4 h-4" /> Copy Content
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="prose prose-blue dark:prose-invert max-w-none">
|
||
<div className="mb-8 p-4 bg-gray-50 dark:bg-dark rounded-lg border-s-4 border-primary italic text-gray-600 dark:text-gray-400">
|
||
<p className="font-bold text-primary mb-1">SEO Blueprint Info:</p>
|
||
<ul className="text-sm space-y-1">
|
||
<li><strong>Meta Title:</strong> {blogData.metaTitle}</li>
|
||
<li><strong>Meta Description:</strong> {blogData.metaDescription}</li>
|
||
<li><strong>Focus Keyword:</strong> {blogData.focusKeyword}</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{blogData.generatedImageUrls?.[0] && (
|
||
<div className="mb-8 rounded-2xl overflow-hidden shadow-2xl relative group">
|
||
<img src={blogData.generatedImageUrls[0]} alt="Generated blog header" className="w-full h-[400px] object-cover transition-transform duration-500 group-hover:scale-105" />
|
||
<div className="absolute top-4 right-4 badge badge-primary bg-primary/80 backdrop-blur-md border-none">AI Generated</div>
|
||
</div>
|
||
)}
|
||
|
||
<h1 className="text-4xl lg:text-5xl font-extrabold mb-10 leading-tight text-black dark:text-white">{blogData.title}</h1>
|
||
<div className="prose prose-lg dark:prose-invert max-w-none
|
||
prose-p:text-lg prose-p:leading-relaxed prose-p:text-gray-700 dark:prose-p:text-gray-300
|
||
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:font-bold
|
||
prose-h3:text-2xl prose-h3:mt-8 prose-h3:font-bold
|
||
prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:py-4 prose-blockquote:px-8 prose-blockquote:rounded-r-2xl prose-blockquote:italic">
|
||
<div dangerouslySetInnerHTML={{ __html: blogData.content }} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Image Prompts Section */}
|
||
<div className="panel shadow-lg">
|
||
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
|
||
<span className="p-2 bg-secondary/10 text-secondary rounded">
|
||
🎨
|
||
</span>
|
||
AI Image Prompts
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{blogData.imagePrompts.map((prompt: string, i: number) => (
|
||
<div key={i} className="p-4 bg-gray-50 dark:bg-dark rounded-xl border border-gray-200 dark:border-gray-800 relative group">
|
||
<p className="text-sm italic text-gray-700 dark:text-gray-300">"{prompt}"</p>
|
||
<button
|
||
onClick={() => copyToClipboard(prompt)}
|
||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-white dark:bg-gray-800 p-1 rounded shadow"
|
||
title="Copy prompt"
|
||
>
|
||
<IconCopy className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sidebar Metrics/Analysis Section */}
|
||
<div className="xl:col-span-4 space-y-6">
|
||
<div className="panel shadow-lg bg-gradient-to-br from-primary/5 to-transparent">
|
||
<h3 className="text-xl font-bold mb-4">Content Quality</h3>
|
||
<div className="space-y-6">
|
||
<div>
|
||
<div className="flex justify-between mb-2">
|
||
<span className="font-semibold">SEO Optimization</span>
|
||
<span className="text-primary font-bold">{blogData.seoScore}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||
<div className="bg-primary h-2.5 rounded-full shadow-[0_0_10px_rgba(67,97,238,0.5)] transition-all duration-1000" style={{ width: `${blogData.seoScore}%` }}></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="flex justify-between mb-2">
|
||
<span className="font-semibold">Human Likelihood Score</span>
|
||
<span className="text-info font-bold text-red-500">{blogData.aiDetectionScore}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||
<div className="bg-red-500 h-2.5 rounded-full shadow-[0_0_10px_rgba(239,68,68,0.5)] transition-all duration-1000" style={{ width: `${blogData.aiDetectionScore}%` }}></div>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div className="flex justify-between mb-2">
|
||
<span className="font-semibold">Human Readability</span>
|
||
<span className="text-secondary font-bold">{blogData.readabilityScore}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||
<div className="bg-secondary h-2.5 rounded-full shadow-[0_0_10px_rgba(128,94,255,0.5)] transition-all duration-1000" style={{ width: `${blogData.readabilityScore}%` }}></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="pt-4 mt-2 border-t border-dashed border-gray-200 dark:border-gray-700">
|
||
<div className="flex justify-between items-center bg-warning/10 p-3 rounded-lg border border-warning/20">
|
||
<span className="font-black text-[10px] uppercase text-warning-dark tracking-widest">Focus Keyword</span>
|
||
<span className="text-xs font-bold text-warning-dark truncate max-w-[150px]">{blogData.focusKeyword || 'AI Optimized'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{blogData.seoDetails && (
|
||
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800">
|
||
<h4 className="text-xs font-bold text-gray-500 uppercase mb-3">SEO Checklist</h4>
|
||
<ul className="space-y-2">
|
||
{blogData.seoDetails.map((detail: string, i: number) => (
|
||
<li key={i} className="text-[11px] flex items-center gap-2">
|
||
<span className={`w-1.5 h-1.5 rounded-full ${detail.toLowerCase().includes('missing') ? 'bg-red-500' : 'bg-green-500'}`}></span>
|
||
<span className={detail.toLowerCase().includes('missing') ? 'text-red-500' : 'text-gray-600 dark:text-gray-400 font-medium'}>{detail}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="panel shadow-lg">
|
||
<h3 className="text-xl font-bold mb-4">Internal Link Strategy</h3>
|
||
<p className="text-sm text-gray-500 mb-4">We've identified the high-authority endpoints to link from this blog post:</p>
|
||
<div className="space-y-3">
|
||
<div className="p-3 bg-gray-50 dark:bg-dark rounded border-l-2 border-primary">
|
||
<p className="text-xs font-bold text-primary uppercase">Primary Anchor</p>
|
||
<p className="text-sm font-medium">Link to "Solutions" page</p>
|
||
</div>
|
||
<div className="p-3 bg-gray-50 dark:bg-dark rounded border-l-2 border-secondary">
|
||
<p className="text-xs font-bold text-secondary uppercase">Secondary Anchor</p>
|
||
<p className="text-sm font-medium">Link to "Our Process" deep dive</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="panel bg-black text-white p-6 rounded-2xl shadow-2xl relative overflow-hidden group">
|
||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-primary/20 rounded-full blur-3xl group-hover:scale-150 transition-all duration-700"></div>
|
||
<h4 className="text-2xl font-bold mb-4 relative z-[1]">Power Up Your Content</h4>
|
||
<p className="text-gray-400 mb-6 relative z-[1]">Upgrade to Pro to unlock unlimited crawls and 100% genuine Gemini-powered content.</p>
|
||
<button className="btn btn-primary w-full py-4 text-lg font-bold relative z-[1] shadow-[0_0_20px_rgba(67,97,238,0.3)]">
|
||
Upgrade Now
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recently Generated Blogs Sidebar */}
|
||
<div className="lg:w-[350px] shrink-0 space-y-6 mt-10 lg:mt-0 px-5 lg:px-0">
|
||
<div className="panel shadow-lg h-fit sticky top-24">
|
||
<div className="flex items-center justify-between mb-4 pb-2 border-b">
|
||
<h3 className="text-xl font-bold">Recent Blogs</h3>
|
||
<button type="button" onClick={fetchRecentBlogs} className="hover:rotate-180 transition-all duration-500 text-primary">
|
||
<IconRefresh className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
|
||
{recentBlogs.length === 0 ? (
|
||
<p className="text-sm text-gray-500 text-center py-10">No blogs generated yet {message ? 'for this query' : ''}.</p>
|
||
) : (
|
||
<div className="space-y-4 max-h-[70vh] overflow-y-auto pr-2 custom-scrollbar">
|
||
{recentBlogs.map((blog) => (
|
||
<div
|
||
key={blog._id}
|
||
className={`p-3 rounded-lg border cursor-pointer transition-all hover:border-primary group ${blogData?._id === blog._id ? 'bg-primary/5 border-primary' : 'bg-gray-50 dark:bg-dark border-transparent'}`}
|
||
onClick={() => setBlogData(blog)}
|
||
>
|
||
<h4 className="font-bold text-sm line-clamp-2 group-hover:text-primary transition-colors">{blog.title}</h4>
|
||
<div className="flex items-center justify-between mt-2">
|
||
<span className="text-[10px] text-gray-400">{new Date(blog.createdAt).toLocaleDateString()}</span>
|
||
<span className="text-[10px] px-2 py-0.5 bg-gray-200 dark:bg-gray-800 rounded-full font-medium">SEO: {blog.seoScore}%</span>
|
||
</div>
|
||
<p className="text-[10px] text-gray-500 mt-1 truncate">{blog.url}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="mt-6 pt-4 border-t">
|
||
<p className="text-[11px] text-gray-400 text-center">
|
||
History is synced to your account.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default BlogGenerator;
|