feat: industry-grade SEO audit engine + plan flow fix (v1.7.0)
This commit is contained in:
@@ -638,12 +638,14 @@ export default function AIAssist() {
|
||||
setInput("");
|
||||
}
|
||||
|
||||
// Capture whether this is the initial plan phase (before any code generation)
|
||||
const wasIdle = !isApproval && (assistStep === "idle" || assistStep === "plan");
|
||||
// Plan-first workflow only applies to the "code" agent (software architect)
|
||||
// SEO, content, smm, design, web, app agents go straight to preview
|
||||
const isCodeAgent = currentAgent === "code" || currentAgent === "general";
|
||||
const wasIdle = !isApproval && isCodeAgent && (assistStep === "idle" || assistStep === "plan");
|
||||
// Detect if user is modifying existing code
|
||||
if (assistStep === "preview") setIsModifying(true);
|
||||
setIsProcessing(true);
|
||||
if (assistStep === "idle") setAssistStep("plan");
|
||||
if (assistStep === "idle" && isCodeAgent) setAssistStep("plan");
|
||||
|
||||
const assistantMsg: AIAssistMessage = {
|
||||
role: "assistant",
|
||||
@@ -695,7 +697,7 @@ export default function AIAssist() {
|
||||
setStatus(null);
|
||||
}
|
||||
|
||||
// SEO mode: auto-fetch URLs from user input for live auditing
|
||||
// SEO mode: auto-fetch URLs from user input for comprehensive live auditing
|
||||
if (currentAgent === "seo") {
|
||||
const urlMatches = [...finalInput.matchAll(/https?:\/\/[^\s<>"')\]]+/gi)].map(m => m[0]);
|
||||
const uniqueUrls = [...new Set(urlMatches)].slice(0, 2);
|
||||
@@ -705,36 +707,72 @@ export default function AIAssist() {
|
||||
for (const url of uniqueUrls) {
|
||||
const auditRes = await fetch("/api/fetch-url?url=" + encodeURIComponent(url));
|
||||
if (auditRes.ok) {
|
||||
const auditData = await auditRes.json();
|
||||
enrichedInput += "\n\n[WEBSITE AUDIT DATA - " + url + "]\n";
|
||||
enrichedInput += "Title: " + (auditData.title || "N/A") + "\n";
|
||||
enrichedInput += "Meta Description: " + (auditData.metaDescription || "N/A") + "\n";
|
||||
enrichedInput += "Meta Keywords: " + (auditData.metaKeywords || "N/A") + "\n";
|
||||
enrichedInput += "Canonical: " + (auditData.canonical || "N/A") + "\n";
|
||||
enrichedInput += "OG Title: " + (auditData.ogTitle || "N/A") + "\n";
|
||||
enrichedInput += "OG Description: " + (auditData.ogDescription || "N/A") + "\n";
|
||||
enrichedInput += "Headings:\n";
|
||||
if (auditData.headings && auditData.headings.length > 0) {
|
||||
for (const h of auditData.headings) {
|
||||
enrichedInput += " H" + h.level + ": " + h.text + "\n";
|
||||
const d = await auditRes.json();
|
||||
enrichedInput += "\n\n[COMPREHENSIVE SEO AUDIT - " + url + "]\n";
|
||||
enrichedInput += "== OVERALL SCORE: " + (d.scores?.overall || "?") + "/100 ==\n";
|
||||
enrichedInput += "Technical: " + (d.scores?.technical || "?") + " | Content: " + (d.scores?.content || "?") + " | Performance: " + (d.scores?.performance || "?") + " | Social: " + (d.scores?.social || "?") + "\n\n";
|
||||
enrichedInput += "== META TAGS ==\n";
|
||||
enrichedInput += "Title: " + (d.title || "MISSING") + " (" + (d.titleLength || 0) + " chars) [" + (d.titleStatus || "?") + "]\n";
|
||||
enrichedInput += "Description: " + (d.metaDescription || "MISSING") + " (" + (d.descLength || 0) + " chars) [" + (d.descStatus || "?") + "]\n";
|
||||
enrichedInput += "Keywords: " + (d.metaKeywords || "None") + "\n";
|
||||
enrichedInput += "Viewport: " + (d.viewport || "MISSING") + " | Charset: " + (d.charset || "None") + "\n";
|
||||
enrichedInput += "Canonical: " + (d.canonical || "None") + (d.hasCanonicalMismatch ? " [MISMATCH!]" : "") + "\n";
|
||||
enrichedInput += "Robots: " + (d.robotsDirectives || "None") + "\n\n";
|
||||
enrichedInput += "== OPEN GRAPH & SOCIAL ==\n";
|
||||
enrichedInput += "OG Title: " + (d.openGraph?.title || "None") + " | OG Desc: " + (d.openGraph?.description || "None") + " | OG Image: " + (d.openGraph?.image || "None") + "\n";
|
||||
enrichedInput += "Twitter Card: " + (d.twitterCard?.card || "None") + "\n\n";
|
||||
enrichedInput += "== HEADINGS ==\n";
|
||||
enrichedInput += "H1 count: " + d.h1Count + " (should be 1) | H2: " + d.h2Count + " | H3: " + d.h3Count + " [" + (d.headingStatus || "?") + "]\n";
|
||||
if (d.headings && d.headings.length > 0) {
|
||||
for (const h of d.headings.slice(0, 15)) enrichedInput += " H" + h.level + ": " + h.text + "\n";
|
||||
}
|
||||
enrichedInput += "\n== LINKS ==\n";
|
||||
const lnks = d.links || {};
|
||||
enrichedInput += "Total: " + (lnks.total || 0) + " | Internal: " + (lnks.internal || 0) + " | External: " + (lnks.external || 0) + " | Nofollow: " + (lnks.nofollow || 0) + "\n";
|
||||
if (lnks.sampleExternal && lnks.sampleExternal.length > 0) {
|
||||
enrichedInput += "Sample external: " + lnks.sampleExternal.slice(0, 10).map((l: { href: string }) => l.href).join(", ") + "\n";
|
||||
}
|
||||
enrichedInput += "\n== IMAGES ==\n";
|
||||
const imgs = d.images || {};
|
||||
enrichedInput += "Total: " + (imgs.total || 0) + " | With alt: " + (imgs.withAlt || 0) + " | Without alt: " + (imgs.withoutAlt || 0) + " | Lazy loaded: " + (imgs.lazyLoaded || 0) + "\n";
|
||||
enrichedInput += "Alt text coverage: " + (imgs.altCoverage || 0) + "%\n";
|
||||
if (imgs.sampleWithoutAlt && imgs.sampleWithoutAlt.length > 0) {
|
||||
enrichedInput += "Missing alt: " + imgs.sampleWithoutAlt.slice(0, 5).join(", ") + "\n";
|
||||
}
|
||||
enrichedInput += "\n== CONTENT ==\n";
|
||||
const cnt = d.content || {};
|
||||
enrichedInput += "Words: " + (cnt.wordCount || 0) + " | Sentences: " + (cnt.sentenceCount || 0) + " | Paragraphs: " + (cnt.paragraphCount || 0) + " | Avg words/sentence: " + (cnt.avgWordsPerSentence || 0) + "\n\n";
|
||||
enrichedInput += "== STRUCTURED DATA ==\n";
|
||||
enrichedInput += "JSON-LD: " + (d.structuredData?.hasJsonLd ? "Yes" : "No") + " | Microdata: " + (d.structuredData?.hasMicrodata ? "Yes" : "No") + "\n";
|
||||
const foundTypes = (d.structuredData?.types || []).filter((t: { found: boolean }) => t.found).map((t: { type: string }) => t.type);
|
||||
enrichedInput += "Schema types found: " + (foundTypes.length > 0 ? foundTypes.join(", ") : "None") + "\n\n";
|
||||
enrichedInput += "== PERFORMANCE SIGNALS ==\n";
|
||||
const perf = d.performance || {};
|
||||
enrichedInput += "Server: " + (d.server || "Unknown") + " | Response time: " + d.responseTime + "ms | HTML size: " + d.htmlSize + " bytes\n";
|
||||
enrichedInput += "External scripts: " + (perf.externalScripts || 0) + " | External stylesheets: " + (perf.externalStylesheets || 0) + " | Inline styles: " + (perf.inlineStyles || 0) + "\n";
|
||||
enrichedInput += "Preconnect: " + (perf.hasPreconnect ? "Yes" : "No") + " | Preload: " + (perf.hasPreload ? "Yes" : "No") + " | DNS prefetch: " + (perf.hasDnsPrefetch ? "Yes" : "No") + "\n";
|
||||
enrichedInput += "Async scripts: " + (perf.usesAsyncScripts ? "Yes" : "No") + " | Defer scripts: " + (perf.usesDeferScripts ? "Yes" : "No") + "\n\n";
|
||||
enrichedInput += "== ACCESSIBILITY ==\n";
|
||||
const acc = d.accessibility || {};
|
||||
enrichedInput += "Lang attr: " + (acc.hasLangAttr ? "Yes" : "No") + " | ARIA labels: " + (acc.hasAriaLabels ? "Yes" : "No") + " | First image alt: " + (acc.hasAltOnFirstImage ? "Yes" : "No") + "\n";
|
||||
if (d.hreflang && d.hreflang.length > 0) {
|
||||
enrichedInput += "\n== HREFLANG ==\n" + d.hreflang.map((h: { lang: string; href: string }) => h.lang + " -> " + h.href).join("\n") + "\n";
|
||||
}
|
||||
enrichedInput += "\n== ISSUES FOUND ==\n";
|
||||
if (d.issues && d.issues.length > 0) {
|
||||
for (const issue of d.issues) {
|
||||
enrichedInput += "[" + issue.severity.toUpperCase() + "] " + issue.category + ": " + issue.message + "\n";
|
||||
}
|
||||
} else {
|
||||
enrichedInput += " None found\n";
|
||||
enrichedInput += "No issues detected.\n";
|
||||
}
|
||||
const internalLinks = (auditData.links || []).filter((l: { internal: boolean }) => l.internal);
|
||||
const externalLinks = (auditData.links || []).filter((l: { internal: boolean }) => !l.internal);
|
||||
enrichedInput += "Links: " + internalLinks.length + " internal, " + externalLinks.length + " external (of " + (auditData.links || []).length + " total)\n";
|
||||
const imagesWithAlt = (auditData.images || []).filter((img: { alt: string }) => img.alt && img.alt.trim());
|
||||
const imagesNoAlt = (auditData.images || []).filter((img: { alt: string }) => !img.alt || !img.alt.trim());
|
||||
enrichedInput += "Images: " + (auditData.images || []).length + " total, " + imagesWithAlt.length + " with alt, " + imagesNoAlt.length + " without alt\n";
|
||||
enrichedInput += "Text content length: " + (auditData.text || "").length + " chars\n";
|
||||
enrichedInput += "HTML size: " + (auditData.htmlLength || 0) + " chars\n";
|
||||
enrichedInput += "[/WEBSITE AUDIT DATA]\n";
|
||||
enrichedInput += "[/COMPREHENSIVE SEO AUDIT]\n";
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn("Website audit failed:", e); }
|
||||
setStatus(null);
|
||||
}
|
||||
|
||||
// If no URL found and web search not enabled, auto-enable web search for SEO
|
||||
if (uniqueUrls.length === 0 && !webSearchEnabled) {
|
||||
try {
|
||||
@@ -812,8 +850,9 @@ export default function AIAssist() {
|
||||
|
||||
if (!response.success) throw new Error(response.error);
|
||||
|
||||
// When this was the initial request from idle/plan, ALWAYS show plan card
|
||||
// Post-stream routing
|
||||
if (wasIdle) {
|
||||
// Code agent: show plan card for approval
|
||||
setAssistStep("plan");
|
||||
const { plan: parsedPlan } = parsePlanFromResponse(accumulated);
|
||||
if (parsedPlan) {
|
||||
@@ -822,10 +861,13 @@ export default function AIAssist() {
|
||||
setAiPlan({ rawText: accumulated, architecture: "", techStack: [], files: [], steps: [] });
|
||||
}
|
||||
} else if ((lastParsedPreview as PreviewData | null)?.data) {
|
||||
// After approval: show the generated preview
|
||||
// Non-code agents or approved code: show the generated preview directly
|
||||
setAssistStep("preview");
|
||||
setShowCanvas(true);
|
||||
if (isPreviewRenderable(lastParsedPreview)) setViewMode("preview");
|
||||
} else if (!isCodeAgent && !wasIdle) {
|
||||
// Non-code agent without preview: just return to idle
|
||||
setAssistStep("idle");
|
||||
} else {
|
||||
setAssistStep("idle");
|
||||
}
|
||||
@@ -1211,7 +1253,7 @@ export default function AIAssist() {
|
||||
</div>
|
||||
|
||||
{/* Agentic Plan Review Card */}
|
||||
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (
|
||||
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (currentAgent === "code" || currentAgent === "general") && (
|
||||
<div className="mt-6 p-6 rounded-2xl bg-blue-500/5 border border-blue-500/20 backdrop-blur-sm animate-in zoom-in-95 duration-300">
|
||||
<h3 className="text-sm font-black text-blue-400 uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<LayoutPanelLeft className="h-4 w-4" /> {t.proposedPlan}
|
||||
|
||||
Reference in New Issue
Block a user