Add AI blog generator page, LLM API route, and sidebar layout component.

This commit is contained in:
Alaguraj0361 2026-01-06 16:48:53 +05:30
parent 1e2892007e
commit dba51ed30b
3 changed files with 150 additions and 58 deletions

View File

@ -11,20 +11,20 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
const BlogGenerator = () => {
const [url, setUrl] = useState('');
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('seomagnifier');
const [selectedEngine, setEngine] = useState('domainnest');
useEffect(() => {
fetchRecentBlogs();
}, [url]);
}, [message]);
const fetchRecentBlogs = async () => {
try {
const { data } = await axios.get(`http://localhost:3020/api/blog/recent${url ? `?url=${encodeURIComponent(url)}` : ''}`);
const { data } = await axios.get(`http://localhost:3020/api/blog/recent`);
if (data.ok) {
setRecentBlogs(data.data);
}
@ -34,25 +34,102 @@ const BlogGenerator = () => {
};
const handleGenerate = async () => {
if (!url) return setError('Please enter a website URL');
if (!message) return setError('Please enter a message or topic');
setLoading(true);
setError('');
setBlogData(null);
try {
const { data } = await axios.post('http://localhost:3020/api/blog/generate', {
url,
engine: selectedEngine
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.ok) {
setBlogData(data.data);
fetchRecentBlogs();
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('Failed to generate content');
setError(data?.error || 'Failed to generate content from DomainNest');
}
} catch (err: any) {
setError(err.response?.data?.error || err.message || 'Something went wrong');
console.error("Generation Error:", err.response?.data || err.message);
setError(err.response?.data?.error || err.message || 'Service is currently unavailable');
} finally {
setLoading(false);
}
@ -82,54 +159,33 @@ const BlogGenerator = () => {
<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">
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="Enter Website URL (e.g., https://example.com)"
className="form-input py-4 ltr:pr-[120px] rtl:pl-[120px] shadow-lg rounded-full border-2 border-primary/20 focus:border-primary transition-all text-lg"
<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 top-1.5 ltr:right-1.5 rtl:left-1.5 rounded-full py-2.5 px-6 shadow-none flex items-center gap-2 ${loading ? 'bg-gray-400 opacity-70' : 'bg-primary'}`}
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>
Analyzing...
<span>Analyzing...</span>
</>
) : (
'Generate Blog'
<>
<span className="text-lg"></span>
<span>Generate</span>
</>
)}
</button>
</div>
<div className="flex flex-wrap items-center justify-center gap-4 animate-fade-in">
<span className="text-xs font-bold uppercase text-gray-400 tracking-wider">AI Engine:</span>
<div className="flex bg-gray-100 dark:bg-dark p-1 rounded-xl border border-gray-200 dark:border-gray-800">
{[
{ id: 'seomagnifier', name: 'SEO Magnifier', icon: '🆓', sub: 'Unlimited' },
{ id: 'ollama', name: 'Ollama', icon: '🏠', sub: 'Local' },
{ id: 'openai', name: 'OpenAI', icon: '💎', sub: 'Premium' },
].map((engine) => (
<button
key={engine.id}
onClick={() => setEngine(engine.id)}
className={`flex flex-col items-center px-4 py-2 rounded-lg transition-all ${selectedEngine === engine.id
? 'bg-white dark:bg-gray-700 shadow-md text-primary scale-105'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
<span className="text-lg">{engine.icon}</span>
<span className="text-[10px] font-bold uppercase">{engine.name}</span>
<span className="text-[8px] opacity-70">{engine.sub}</span>
</button>
))}
</div>
</div>
</div>
{error && <p className="text-red-500 mt-4 text-center font-medium animate-bounce">{error}</p>}
</div>
@ -141,16 +197,16 @@ const BlogGenerator = () => {
<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 URL above to start the magic.</p>
<p className="text-sm">We'll crawl the site, analyze its SEO structure, and build a custom blog for you.</p>
<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">Deep Auditing Website Structure...</p>
<p className="text-gray-500">Extracting semantic markers and entity relationships...</p>
<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>
)}
@ -194,9 +250,7 @@ const BlogGenerator = () => {
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">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{blogData.content}
</ReactMarkdown>
<div dangerouslySetInnerHTML={{ __html: blogData.content }} />
</div>
</div>
</div>
@ -242,11 +296,11 @@ const BlogGenerator = () => {
</div>
<div>
<div className="flex justify-between mb-2">
<span className="font-semibold">AI Detection (Human Score)</span>
<span className="text-info font-bold">{blogData.aiDetectionScore || 92}%</span>
<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-info h-2.5 rounded-full shadow-[0_0_10px_rgba(0,186,211,0.5)] transition-all duration-1000" style={{ width: `${blogData.aiDetectionScore || 92}%` }}></div>
<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>
@ -322,7 +376,7 @@ const BlogGenerator = () => {
</div>
{recentBlogs.length === 0 ? (
<p className="text-sm text-gray-500 text-center py-10">No blogs generated yet {url ? 'for this URL' : ''}.</p>
<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) => (

30
app/api/llm/route.ts Normal file
View File

@ -0,0 +1,30 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const body = await request.json();
const response = await fetch('https://llm.thedomainnest.com/chat-json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
console.error('LLM Proxy External Error:', response.status, errorText);
return NextResponse.json({
ok: false,
error: `External API error (${response.status}). The service might be overloaded or the mode "${body.mode}" is not supported.`
}, { status: response.status });
}
const data = await response.json();
return NextResponse.json(data);
} catch (error: any) {
console.error('LLM Proxy Exception:', error);
return NextResponse.json({ ok: false, error: error.message }, { status: 500 });
}
}

View File

@ -157,6 +157,14 @@ const Sidebar = () => {
</div>
</Link>
</li>
<li className="nav-item">
<Link href="/page-speed-test" className="group">
<div className="flex items-center">
<IconMenuTodo className="shrink-0 group-hover:!text-primary" />
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Website Speed Test</span>
</div>
</Link>
</li>
{/* <li className="nav-item">
<Link href="/apps/mailbox" className="group">
<div className="flex items-center">