411 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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) 35 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;