Compare commits

..

25 Commits

15 changed files with 1817 additions and 366 deletions

1
.gitignore vendored
View File

@@ -37,3 +37,4 @@ next-env.d.ts
# logs # logs
logs logs
*.log *.log
fix_*.py

View File

@@ -3,7 +3,187 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning]
## [2.2.1] - 2026-03-19 05:24 UTC
### Added
- **Leads Canvas Preview** - Leads Finder now renders results as an Excel-like HTML table in the canvas, with columns: #, Name, Platform, Followers, Region, Bio, Link
- **Dark-Themed Leads Report** - Beautiful dark UI with emerald accents, stats grid (total leads, combined reach, top platform, top region), platform-colored badges (Instagram, Twitter, LinkedIn, YouTube, TikTok), and hover effects
- **Auto Canvas** - Leads Finder is now a visual agent that auto-opens the canvas when preview data is available
### Fixed
- **Build Error** - Restored `<Badge variant="outline">` for plan card tech stack divs that were incorrectly replaced with plain `<div>` by an over-broad fix script
### Technical Details
- Files modified: 2 (AIAssist.tsx, openrouter.ts)
## [2.2.0] - 2026-03-19 04:44 UTC
### Added
- **Leads Finder Agent** - New agent mode in Vibe Architect that finds relevant influencers, prospects, and leads across social media platforms (Instagram, Twitter/X, LinkedIn, YouTube, TikTok)
- **Auto Web Search** - Leads Finder automatically enables web search to find real-time leads data
- **Structured Lead Format** - Returns leads in consistent format: Name | Followers | Region | Location with description and social URL
### Technical Details
- Files modified: 3 (AIAssist.tsx, translations.ts, openrouter.ts)
## [2.1.0] - 2026-03-18 22:06 UTC
### Added
- **Comprehensive 17-Section Report** — Full rebuild of SEO/GEO audit report generator with all industry-standard sections:
- Executive Summary with stats grid
- 6-Category Scoring Breakdown with progress bars (Overall, Technical, Content, Performance, Social, GEO)
- Meta Tags Analysis (9 checks: title, description, keywords, viewport, charset, canonical, robots, X-Frame-Options, protocol)
- Social & Open Graph (8 properties with pass/fail status)
- Heading Structure with hierarchy visualization
- Links Analysis with sample external links table
- Images & Alt Text with missing-alt report
- Content Analysis with readability recommendations
- Performance Signals (12 metrics including encoding, scripts, preconnect, preload, DNS prefetch, async/defer)
- Accessibility (4 checks: lang, ARIA, first-image alt, alt coverage)
- Structured Data / Schema detection
- Hreflang Tags
- GEO Analysis with readiness score, 4 sub-scores (Factual, Entity, Content Depth, Citeability), and improvement checklist
- Issues & How to Fix (severity badges, category-specific fix instructions)
- Prioritized Action Plan (high/medium/low with impact labels)
- Recommended FAQ Schema Questions (auto-generated based on domain)
- SEO/GEO Best Practices Checklist (12 items)
- **GEO Scoring Engine** — 0-100 GEO readiness score based on schema markup, content depth, heading hierarchy, and citeability signals
- **Auto-Generated Action Plan** — 20+ prioritized fixes derived from audit data with severity and impact labels
- **Print-Optimized CSS** — Clean white-on-dark print styles for professional PDF output
### Technical Details
- Files modified: 1 (seo-report.ts) — complete rewrite, 530+ lines
## [2.0.1] - 2026-03-18 21:56 UTC
### Fixed
- **PDF Export** — Replaced `window.open` with hidden iframe approach for reliable print dialog trigger (no popup blocker issues)
- **PDF Export** — Initial fix using Blob URL (superseded by iframe approach)
### Added
- **Inline SEO Export Buttons** — Export HTML/PDF buttons now appear directly in chat messages after SEO audit, not just in post-coding action bar
### Technical Details
- Files modified: 2 (AIAssist.tsx, seo-report.ts)
## [2.0.0] - 2026-03-18 21:33 UTC
### Added
- **SEO Audit Export** — Export SEO/GEO audit reports as HTML or PDF with comprehensive fix instructions
- **SEO Report Generator** — Standalone `lib/seo-report.ts` utility with color-coded scores, issue tables with fix instructions, and print-friendly CSS
- **Default Vibe Architect** — Welcome screen now opens to Vibe Architect by default (was Prompt Enhancer)
- **Vibe Architect Dedicated Route** — Full-screen immersive mode at `/vibe` with `vibeMode` prop
### Changed
- **General Agent Plain Chat** — General mode no longer triggers plan/code flow, now works as plain chat
- **SEO Follow-up Fix** — Non-visual agents (SEO, content, SMM) preserve existing canvas on follow-up messages
### Technical Details
- Files modified: 3 (AIAssist.tsx, page.tsx)
- Files added: 2 (seo-report.ts, vibe/page.tsx)
(https://semver.org/spec/v2.0.0.html).
## [1.9.0] - 2026-03-18 21:05 UTC
### Changed
- **Global Rebrand: AI Assist -> Vibe Architect** — All instances renamed across UI, sidebar, translations (EN/RU/HE)
- **Sidebar Highlight** — Vibe Architect button now has a gradient blue-purple border and bold blue text when not active, making it stand out in the navigation menu
### Technical Details
- Files modified: 3 (Sidebar.tsx, AIAssist.tsx, translations.ts)
## [1.8.0] - 2026-03-18 21:02 UTC
### Added
- **Vibe Architect Dedicated Mode** — Full-screen immersive AI coding experience
- New route: `/vibe` ([rommark.dev/tools/promptarch/vibe/](https://rommark.dev/tools/promptarch/vibe/))
- No sidebar, no navigation — just AI Assist in full screen
- Rebranded as "Vibe Architect" with dedicated messaging
- `vibeMode` prop on AIAssist component for label overrides
### Fixed
- **SEO follow-up preview** — Canvas no longer breaks when asking follow-up questions after SEO audit
- `isModifying` overlay only triggers for visual agents (code, web, app, design) — not SEO/content/SMM
- Non-visual agent follow-ups preserve existing canvas if no new preview is generated
### Technical Details
- Files added: 1 (`app/vibe/page.tsx`)
- Files modified: 1 (AIAssist.tsx: +8/-3 lines)
- New prop: `vibeMode` on AIAssist component
## [1.7.0] - 2026-03-18 20:44 UTC
### Added
- **Comprehensive SEO Audit Engine** — Industry-grade URL analysis modeled after Screaming Frog, Ahrefs, Sitebulb, and Semrush
- Full meta tag analysis: title (length/status), description (length/status), keywords, viewport, charset, robots directives
- Open Graph & Twitter Card validation
- Heading structure audit: H1 count check, hierarchy analysis (H1-H6), multiple H1 detection
- Link analysis: internal/external/nofollow counts, external link sampling
- Image audit: total count, alt text coverage %, lazy loading detection, missing alt samples
- Content analysis: word count, sentence count, paragraph count, avg words/sentence
- Structured data detection: JSON-LD, Microdata, 12+ schema types (Article, FAQ, Product, LocalBusiness, etc.)
- Performance signals: server info, response time, HTML size, external scripts/stylesheets, inline styles, preconnect/preload/dns-prefetch, async/defer detection
- Accessibility checks: lang attribute, ARIA labels, first image alt text
- Hreflang tag detection for international SEO
- Canonical tag validation with mismatch detection
- **Automated scoring**: Overall, Technical, Content, Performance, Social scores (0-100)
- **Issue detection**: Critical/Warning/Info severity levels across all categories
- **Plan-First Flow Fix** — Non-code agents (SEO, content, SMM, design, web, app) now bypass the plan-first workflow
- No more "Start Coding" or "Modify Plan" buttons for non-code agent responses
- SEO agent goes straight to preview mode after generating audit reports
- Plan card only shown for code/general agents
### Changed
- `/api/fetch-url` route completely rewritten: 300+ lines of comprehensive HTML parsing and scoring
- SEO agent data injection now passes structured audit data with scores, issues, and category breakdowns
### Technical Details
- Files modified: 5 (AIAssist.tsx, qwen-oauth.ts, ollama-cloud.ts, zai-plan.ts, openrouter.ts)
- Files rewritten: 1 (`app/api/fetch-url/route.ts` — complete rewrite)
- New component-level guard: `isCodeAgent` condition for plan-first workflow
## [1.6.0] - 2026-03-18 20:34 UTC
### Added
- **SEO Web Audit** — SEO agent can now fetch and analyze live websites
- `/api/fetch-url` route extracts: title, meta tags, headings (h1-h6), links (internal/external), images (alt text coverage), canonical URL, OG tags, text content length
- Auto-detects URLs in user input when SEO agent is active
- Pre-fetches page data before sending to AI for comprehensive audit reports
- Supports up to 2 URLs per request
- **SEO Auto Web Search** — When SEO agent is active and no URL is provided, web search is automatically triggered
- Uses same SearXNG infrastructure as manual web search toggle
- No manual toggle needed in SEO mode
- **Updated SEO Agent Prompt** — All 4 AI services now instruct SEO agent about web audit and search capabilities
- Added `[WEB_AUDIT:url]` and `[WEB_SEARCH:query]` tool markers
- Added "WEB TOOLS" section to system prompts
### Technical Details
- Files modified: 5 (AIAssist.tsx, qwen-oauth.ts, ollama-cloud.ts, zai-plan.ts, openrouter.ts)
- Files added: 1 (`app/api/fetch-url/route.ts`)
- New API endpoint: `GET /api/fetch-url?url=https://example.com`
## [1.5.0] - 2026-03-18 20:29 UTC
### Added
- **Modification Progress Overlay** — Visually appealing spinner when modifying existing generated code
- Full-screen canvas overlay with spinning ring + Wrench icon
- "Modification in Progress" label with animated bouncing dots
- Canvas hidden during modification, revealed only when AI finishes
- `isModifying` state tracked via `assistStep === "preview"` detection
### Fixed
- **Preview blink during modification** — Old preview no longer flashes while AI rewrites code
### Technical Details
- Files modified: 1 (AIAssist.tsx: +21/-2 lines)
- New state:
- New import: from lucide-react
## [1.4.0] - 2026-03-18 19:57 UTC ## [1.4.0] - 2026-03-18 19:57 UTC

View File

@@ -1,5 +1,7 @@
# PromptArch: AI Orchestration Platform # PromptArch: AI Orchestration Platform
> **Latest Version**: [v2.2.1](CHANGELOG.md#2.2.1---2026-03-19) (2026-03-19)(CHANGELOG.md#2.1.0---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.1---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.0---2026-03-18) (2026-03-18)(CHANGELOG.md#190---2026-03-18) (2026-03-18)
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW). > **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).** > **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**
@@ -19,7 +21,7 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an A
| Feature | Description | | Feature | Description |
|---------|-------------| |---------|-------------|
| **AI Assist** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview | | **Vibe Architect** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview |
| **Prompt Enhancer** | Refine vague prompts into surgical instructions using 9 enhancement strategies and 11+ intent patterns | | **Prompt Enhancer** | Refine vague prompts into surgical instructions using 9 enhancement strategies and 11+ intent patterns |
| **PRD Generator** | Convert ideas into structured Product Requirements Documents | | **PRD Generator** | Convert ideas into structured Product Requirements Documents |
| **Action Plan** | Decompose PRDs into actionable development steps and framework recommendations | | **Action Plan** | Decompose PRDs into actionable development steps and framework recommendations |
@@ -141,6 +143,15 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
| Version | Date | Highlights | | Version | Date | Highlights |
|---------|------|------------| |---------|------|------------|
| [2.2.0](CHANGELOG.md#2.2.0---2026-03-19) | Leads Finder Agent Mode | 2026-03-19 |
| [2.1.0](CHANGELOG.md#2.1.0---2026-03-18) | Full 17-Section Report, GEO Scoring, Action Plan, FAQ Gen | 2026-03-18 |
| [2.0.1](CHANGELOG.md#2.0.1---2026-03-18) | Inline SEO Export, PDF Print Fix | 2026-03-18 |
| [2.0.0](CHANGELOG.md#2.0.0---2026-03-18) | SEO Export, Default Vibe, /vibe Route, General Chat | 2026-03-18 |
| [1.9.0](CHANGELOG.md#190---2026-03-18) | 2026-03-18 21:05 UTC | Vibe Architect rebrand, sidebar highlight |
| [1.8.0](CHANGELOG.md#180---2026-03-18) | 2026-03-18 21:02 UTC | Vibe Architect dedicated mode, SEO follow-up fix |
| [1.7.0](CHANGELOG.md#170---2026-03-18) | 2026-03-18 20:44 UTC | Industry-grade SEO audit, plan flow fix for non-code agents |
| [1.6.0](CHANGELOG.md#160---2026-03-18) | 2026-03-18 20:34 | SEO web audit, URL fetching, auto web search for SEO mode |
| [1.5.0](CHANGELOG.md#150---2026-03-18) | 2026-03-18 20:29 | Modification progress overlay, preview blink fix |
| [1.4.0](CHANGELOG.md#140---2026-03-18) | 2026-03-18 19:57 | Review Code button, web search grounding, responsive preview, model selector fix | | [1.4.0](CHANGELOG.md#140---2026-03-18) | 2026-03-18 19:57 | Review Code button, web search grounding, responsive preview, model selector fix |
| [1.3.0](CHANGELOG.md#130---2026-03-18) | 2026-03-18 18:51 | Plan-first workflow, OpenRouter, post-coding UX, enhanced prompt engine | | [1.3.0](CHANGELOG.md#130---2026-03-18) | 2026-03-18 18:51 | Plan-first workflow, OpenRouter, post-coding UX, enhanced prompt engine |
| [1.2.0](CHANGELOG.md#120---2026-01-19) | 2026-01-19 19:16 | SEO agent fixes, Z.AI API validation | | [1.2.0](CHANGELOG.md#120---2026-01-19) | 2026-01-19 19:16 | SEO agent fixes, Z.AI API validation |

269
app/api/fetch-url/route.ts Normal file
View File

@@ -0,0 +1,269 @@
/**
* Next.js API route: Fetch URL content for comprehensive SEO/GEO auditing.
* Endpoint: GET /api/fetch-url?url=https://example.com
* Returns: Full SEO audit data (technical, content, performance signals, accessibility)
*/
import { NextRequest, NextResponse } from "next/server";
function countOccurrences(text: string, regex: RegExp): number {
return (text.match(regex) || []).length;
}
export async function GET(request: NextRequest) {
const targetUrl = request.nextUrl.searchParams.get("url");
if (!targetUrl) {
return NextResponse.json({ error: "URL parameter required" }, { status: 400 });
}
let normalizedUrl = targetUrl;
if (!normalizedUrl.startsWith("http")) normalizedUrl = "https://" + normalizedUrl;
try {
new URL(normalizedUrl);
} catch {
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
}
const startTime = Date.now();
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15000);
const res = await fetch(normalizedUrl, {
signal: controller.signal,
headers: {
"User-Agent": "Mozilla/5.0 (compatible; PromptArch-SEOAudit/1.6; +https://rommark.dev)",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
},
});
clearTimeout(timeout);
const responseTime = Date.now() - startTime;
if (!res.ok) {
return NextResponse.json({ error: `HTTP ${res.status}`, status: res.status, url: normalizedUrl, responseTime });
}
const html = await res.text();
const urlObj = new URL(normalizedUrl);
const baseDomain = urlObj.hostname;
// === HTTP HEADERS ===
const headers: Record<string, string> = {};
res.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
const isHttps = normalizedUrl.startsWith("https://");
const server = headers["server"] || "Unknown";
const xFrameOptions = headers["x-frame-options"] || null;
const contentEncoding = headers["content-encoding"] || "";
// === ROBOTS & CANONICAL ===
const robotsMeta = html.match(/<meta[^>]*name\s*=\s*["']robots["'][^>]*content\s*=\s*["']([\s\S]*?)["']/i)
|| html.match(/<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']robots["']/i);
const robotsDirectives = robotsMeta ? robotsMeta[1].trim() : null;
const canonicalMatch = html.match(/<link[^>]*rel\s*=\s*["']canonical["'][^>]*href\s*=\s*["']([^"']*)["']/i)
|| html.match(/<link[^>]*href\s*=\s*["']([^"']*)["'][^>]*rel\s*=\s*["']canonical["']/i);
const canonical = canonicalMatch ? canonicalMatch[1] : null;
const hasCanonicalMismatch = canonical && canonical !== normalizedUrl && !canonical.startsWith("/") && new URL(canonical).href !== new URL(normalizedUrl).href;
// === META TAGS ===
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
const title = titleMatch ? titleMatch[1].trim() : null;
const titleLength = title ? title.length : 0;
const descMatch = html.match(/<meta[^>]*name\s*=\s*["']description["'][^>]*content\s*=\s*["']([\s\S]*?)["']/i)
|| html.match(/<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']description["']/i);
const metaDescription = descMatch ? descMatch[1].trim() : null;
const descLength = metaDescription ? metaDescription.length : 0;
const kwMatch = html.match(/<meta[^>]*name\s*=\s*["']keywords["'][^>]*content\s*=\s*["']([\s\S]*?)["']/i)
|| html.match(/<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']keywords["']/i);
const metaKeywords = kwMatch ? kwMatch[1].trim() : null;
const viewportMatch = html.match(/<meta[^>]*name\s*=\s*["']viewport["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const viewport = viewportMatch ? viewportMatch[1].trim() : null;
const charsetMatch = html.match(/<meta[^>]*charset\s*=\s*["']?([^"'\s>]+)/i)
|| html.match(/<meta[^>]*content\s*=\s*["'][^"']*charset=([^"'\s]+)/i);
const charset = charsetMatch ? charsetMatch[1].trim() : null;
// === OPEN GRAPH ===
const ogTitle = html.match(/<meta[^>]*property\s*=\s*["']og:title["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const ogDesc = html.match(/<meta[^>]*property\s*=\s*["']og:description["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const ogImage = html.match(/<meta[^>]*property\s*=\s*["']og:image["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const ogType = html.match(/<meta[^>]*property\s*=\s*["']og:type["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const ogUrl = html.match(/<meta[^>]*property\s*=\s*["']og:url["'][^>]*content\s*=\s*["']([^"']*)["']/i);
// === TWITTER CARD ===
const twCard = html.match(/<meta[^>]*name\s*=\s*["']twitter:card["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const twTitle = html.match(/<meta[^>]*name\s*=\s*["']twitter:title["'][^>]*content\s*=\s*["']([^"']*)["']/i);
const twDesc = html.match(/<meta[^>]*name\s*=\s*["']twitter:description["'][^>]*content\s*=\s*["']([^"']*)["']/i);
// === HEADING STRUCTURE ===
const headings: { level: number; text: string }[] = [];
const headingRegex = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
let hMatch;
while ((hMatch = headingRegex.exec(html)) !== null) {
const text = hMatch[2].replace(/<[^>]*>/g, "").trim();
if (text) headings.push({ level: parseInt(hMatch[1]), text });
}
const h1Count = headings.filter(h => h.level === 1).length;
const h2Count = headings.filter(h => h.level === 2).length;
const h3Count = headings.filter(h => h.level === 3).length;
const h4Count = headings.filter(h => h.level === 4).length;
const headingHierarchy = headings.map(h => ({ level: h.level, text: h.text.substring(0, 100) }));
// === LINKS ===
const links: { href: string; text: string; internal: boolean; nofollow: boolean }[] = [];
const linkRegex = /<a[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi;
let lMatch;
while ((lMatch = linkRegex.exec(html)) !== null) {
const href = lMatch[1].trim();
const fullTag = lMatch[0];
const text = lMatch[2].replace(/<[^>]*>/g, "").trim().substring(0, 100);
if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:")) continue;
const isNofollow = /rel\s*=\s*["'][^"']*nofollow/i.test(fullTag);
try {
const linkDomain = new URL(href, normalizedUrl).hostname;
links.push({ href, text, internal: linkDomain === baseDomain, nofollow: isNofollow });
} catch { continue; }
}
const internalLinks = links.filter(l => l.internal);
const externalLinks = links.filter(l => !l.internal);
const nofollowLinks = links.filter(l => l.nofollow);
// === IMAGES ===
const images: { src: string; alt: string; loading?: string }[] = [];
const imgRegex = /<img([^>]*)\/?>/gi;
let iMatch;
while ((iMatch = imgRegex.exec(html)) !== null) {
const attrs = iMatch[1];
const srcMatch = attrs.match(/src\s*=\s*["']([^"']*)["']/i);
const altMatch = attrs.match(/alt\s*=\s*["']([^"']*)["']/i);
const loadMatch = attrs.match(/loading\s*=\s*["']([^"']*)["']/i);
if (srcMatch) {
images.push({ src: srcMatch[1], alt: altMatch ? altMatch[1] : "", loading: loadMatch ? loadMatch[1] : undefined });
}
}
const imagesWithAlt = images.filter(img => img.alt && img.alt.trim().length > 0);
const imagesWithoutAlt = images.filter(img => !img.alt || !img.alt.trim());
const lazyLoadedImages = images.filter(img => img.loading === "lazy");
// === CONTENT ANALYSIS ===
const plainText = html
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<noscript[\s\S]*?<\/noscript>/gi, "")
.replace(/<[^>]*>/g, " ")
.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<")
.replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'")
.replace(/\s+/g, " ").trim();
const wordCount = plainText ? plainText.split(/\s+/).length : 0;
const sentenceCount = plainText ? (plainText.match(/[.!?]+/g) || []).length : 0;
const paragraphCount = plainText ? (plainText.match(/\n\s*\n/g) || []).length : 0;
const avgWordsPerSentence = sentenceCount > 0 ? Math.round(wordCount / sentenceCount) : 0;
// === STRUCTURED DATA ===
const sdTypes = ["Article", "BlogPosting", "FAQPage", "HowTo", "Product", "LocalBusiness", "Organization", "BreadcrumbList", "WebSite", "SearchAction", "VideoObject", "Review"];
const structuredData = sdTypes.map(sdType => ({
type: sdType,
found: new RegExp('"@type"\\s*:\\s*"' + sdType + '"', "i").test(html)
|| new RegExp('"@type"\\s*:\\s*\\["' + sdType + '"', "i").test(html),
}));
const hasJsonLd = /<script[^>]*type\s*=\s*["']application\/ld\+json["']/i.test(html);
const hasMicrodata = /itemscope/i.test(html);
// === HREFLANG ===
const hreflangTags: { lang: string; href: string }[] = [];
const hlRegex = /<link[^>]*rel\s*=\s*["']alternate["'][^>]*hreflang\s*=\s*["']([^"']*)["'][^>]*href\s*=\s*["']([^"']*)["']/i;
let hlMatch;
while ((hlMatch = hlRegex.exec(html)) !== null) {
hreflangTags.push({ lang: hlMatch[1], href: hlMatch[2] });
}
// === PERFORMANCE SIGNALS ===
const htmlSize = html.length;
const inlineStyleCount = countOccurrences(html, /style\s*=\s*"/g);
const inlineScriptCount = countOccurrences(html, /<script(?!.*src)/gi);
const externalScripts = countOccurrences(html, /<script[^>]*src\s*=/gi);
const externalStylesheets = countOccurrences(html, /<link[^>]*stylesheet/gi);
const hasPreconnect = /<link[^>]*rel\s*=\s*["']preconnect["']/i.test(html);
const hasPreload = /<link[^>]*rel\s*=\s*["']preload["']/i.test(html);
const hasDnsPrefetch = /<link[^>]*rel\s*=\s*["']dns-prefetch["']/i.test(html);
const usesAsyncScripts = /async\s*=/.test(html);
const usesDeferScripts = /defer\s*=/.test(html);
// === ACCESSIBILITY ===
const hasLangAttr = /<html[^>]*lang\s*=/i.test(html);
const hasAriaLabels = /aria-label|aria-labelledby|aria-describedby/i.test(html);
// === SCORE CALCULATION ===
let score = 100;
const issues: { severity: "critical" | "warning" | "info"; category: string; message: string }[] = [];
if (!title) { score -= 10; issues.push({ severity: "critical", category: "Meta", message: "Missing title tag" }); }
else if (titleLength > 60) { score -= 3; issues.push({ severity: "warning", category: "Meta", message: "Title too long (" + titleLength + " chars, max 60)" }); }
if (!metaDescription) { score -= 10; issues.push({ severity: "critical", category: "Meta", message: "Missing meta description" }); }
else if (descLength > 160) { score -= 3; issues.push({ severity: "warning", category: "Meta", message: "Meta description too long (" + descLength + " chars, max 160)" }); }
if (h1Count === 0) { score -= 10; issues.push({ severity: "critical", category: "Content", message: "Missing H1 heading" }); }
if (h1Count > 1) { score -= 5; issues.push({ severity: "critical", category: "Content", message: "Multiple H1 tags (" + h1Count + " found)" }); }
if (!viewport) { score -= 10; issues.push({ severity: "critical", category: "Mobile", message: "Missing viewport meta tag" }); }
if (!isHttps) { score -= 10; issues.push({ severity: "critical", category: "Security", message: "Not using HTTPS" }); }
if (imagesWithoutAlt.length > 0) { score -= 5; issues.push({ severity: "warning", category: "Accessibility", message: imagesWithoutAlt.length + " images missing alt text" }); }
if (!canonical) { score -= 3; issues.push({ severity: "warning", category: "Technical", message: "Missing canonical tag" }); }
if (hasCanonicalMismatch) { score -= 5; issues.push({ severity: "warning", category: "Technical", message: "Canonical URL mismatch" }); }
if (inlineStyleCount > 10) { score -= 3; issues.push({ severity: "warning", category: "Performance", message: inlineStyleCount + " inline styles detected" }); }
if (!hasPreconnect && externalScripts > 3) { score -= 3; issues.push({ severity: "warning", category: "Performance", message: "Missing preconnect hints for external resources" }); }
if (wordCount < 300 && wordCount > 0) { score -= 3; issues.push({ severity: "warning", category: "Content", message: "Thin content (" + wordCount + " words)" }); }
if (!ogTitle && !ogDesc) { score -= 3; issues.push({ severity: "warning", category: "Social", message: "Missing Open Graph tags" }); }
if (!twCard) { score -= 2; issues.push({ severity: "warning", category: "Social", message: "Missing Twitter Card tags" }); }
if (externalLinks.length === 0) { score -= 2; issues.push({ severity: "warning", category: "Links", message: "No external links found" }); }
if (robotsDirectives && /noindex/i.test(robotsDirectives)) { score -= 10; issues.push({ severity: "critical", category: "Technical", message: "Page has noindex directive" }); }
if (!hasJsonLd && !hasMicrodata) { score -= 1; issues.push({ severity: "info", category: "Structured Data", message: "No structured data found" }); }
if (!hasLangAttr) { score -= 1; issues.push({ severity: "info", category: "Accessibility", message: "Missing html lang attribute" }); }
if (!lazyLoadedImages.length && images.length > 5) { score -= 2; issues.push({ severity: "info", category: "Performance", message: "Consider lazy loading for images" }); }
score = Math.max(0, Math.min(100, score));
const technicalScore = Math.min(100, 100 - issues.filter(i => i.category === "Technical" || i.category === "Security").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
const contentScore = Math.min(100, 100 - issues.filter(i => i.category === "Content").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
const performanceScore = Math.min(100, 100 - issues.filter(i => i.category === "Performance" || i.category === "Mobile").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
const socialScore = Math.min(100, 100 - issues.filter(i => i.category === "Social" || i.category === "Structured Data").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
return NextResponse.json({
url: normalizedUrl,
domain: baseDomain,
protocol: isHttps ? "HTTPS" : "HTTP",
responseTime,
server,
htmlSize,
title, titleLength,
titleStatus: !title ? "missing" : titleLength > 60 ? "too_long" : "good",
metaDescription, descLength,
descStatus: !metaDescription ? "missing" : descLength > 160 ? "too_long" : "good",
metaKeywords, viewport, charset, robotsDirectives,
canonical, hasCanonicalMismatch, xFrameOptions,
openGraph: { title: ogTitle ? ogTitle[1] : null, description: ogDesc ? ogDesc[1] : null, image: ogImage ? ogImage[1] : null, type: ogType ? ogType[1] : null, url: ogUrl ? ogUrl[1] : null },
twitterCard: { card: twCard ? twCard[1] : null, title: twTitle ? twTitle[1] : null, description: twDesc ? twDesc[1] : null },
headings: headingHierarchy, h1Count, h2Count, h3Count, h4Count,
headingStatus: h1Count === 0 ? "missing_h1" : h1Count > 1 ? "multiple_h1" : "good",
links: { total: links.length, internal: internalLinks.length, external: externalLinks.length, nofollow: nofollowLinks.length, sampleExternal: externalLinks.slice(0, 20).map(l => ({ href: l.href, text: l.text, nofollow: l.nofollow })) },
images: { total: images.length, withAlt: imagesWithAlt.length, withoutAlt: imagesWithoutAlt.length, lazyLoaded: lazyLoadedImages.length, altCoverage: images.length > 0 ? Math.round((imagesWithAlt.length / images.length) * 100) : 100, sampleWithoutAlt: imagesWithoutAlt.slice(0, 10).map(img => img.src) },
content: { wordCount, sentenceCount, paragraphCount, avgWordsPerSentence, textPreview: plainText.substring(0, 2000) },
structuredData: { hasJsonLd, hasMicrodata, types: structuredData },
hreflang: hreflangTags,
performance: { inlineStyles: inlineStyleCount, inlineScripts: inlineScriptCount, externalScripts, externalStylesheets, hasPreconnect, hasPreload, hasDnsPrefetch, usesAsyncScripts, usesDeferScripts, contentEncoding },
accessibility: { hasLangAttr, hasAriaLabels, hasAltOnFirstImage: images.length > 0 && images[0].alt && images[0].alt.trim().length > 0 },
scores: { overall: score, technical: technicalScore, content: contentScore, performance: performanceScore, social: socialScore },
issues,
});
} catch (error) {
const msg = error instanceof Error ? error.message : "Fetch failed";
return NextResponse.json({ error: msg }, { status: 500 });
}
}

View File

@@ -20,7 +20,7 @@ const HistoryPanel = dynamic(() => import("@/components/HistoryPanel"), { ssr: f
const SettingsPanel = dynamic(() => import("@/components/SettingsPanel"), { ssr: false }); const SettingsPanel = dynamic(() => import("@/components/SettingsPanel"), { ssr: false });
export default function Home() { export default function Home() {
const [currentView, setCurrentView] = useState<View>("enhance"); const [currentView, setCurrentView] = useState<View>("ai-assist");
useEffect(() => { useEffect(() => {
console.log("[Home] Initializing Qwen OAuth service on client..."); console.log("[Home] Initializing Qwen OAuth service on client...");

20
app/vibe/page.tsx Normal file
View File

@@ -0,0 +1,20 @@
"use client";
import { useEffect } from "react";
import dynamic from 'next/dynamic';
import modelAdapter from "@/lib/services/adapter-instance";
const AIAssist = dynamic(() => import("@/components/AIAssist"), { ssr: false });
export default function VibePage() {
useEffect(() => {
console.log("[Vibe] Initializing services...");
modelAdapter["qwenService"]["initialize"]?.();
}, []);
return (
<div className="fixed inset-0 bg-[#050508] overflow-hidden">
<AIAssist vibeMode />
</div>
);
}

View File

@@ -1,10 +1,11 @@
"use client"; "use client";
import React, { useState, useEffect, useRef, memo } from "react"; import React, { useState, useEffect, useRef, memo } from "react";
import { downloadSeoReport } from "@/lib/seo-report";
import { import {
MessageSquare, Send, Code2, Palette, Search, MessageSquare, Send, Code2, Palette, Search,
Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost, Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost,
Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key, ShieldCheck Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key, ShieldCheck, Wrench, FileText, Users
} from "lucide-react"; } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -361,7 +362,7 @@ function parseStreamingContent(text: string, currentAgent: string) {
// 3. Clean display text - hide all tag-like sequences and their partials // 3. Clean display text - hide all tag-like sequences and their partials
chatDisplay = text chatDisplay = text
// Hide complete tags (flexible brackets), including SUGGEST_AGENT // Hide complete tags (flexible brackets), including SUGGEST_AGENT
.replace(/\[+(?:AGENT|SUGGEST_AGENT|content|seo|smm|pm|code|design|web|app|PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT|PREV):?[\w-]*:?[\w-]*\]+/gi, "") .replace(/\[+(?:AGENT|SUGGEST_AGENT|content|seo|smm|pm|code|design|web|app|PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT|PREV|WEB_AUDIT|WEB_SEARCH):?[\w-]*:?https?:\/\/[^\]]*\]+/gi, "").replace(/\[+(?:AGENT|SUGGEST_AGENT|content|seo|smm|pm|code|design|web|app|PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT|PREV|WEB_AUDIT|WEB_SEARCH):?[\w-]*:?[\w-]*\]+/gi, "")
// Hide content inside preview block (cleanly) // Hide content inside preview block (cleanly)
.replace(/\[+PREVIEW:[\w-]+:?[\w-]+?\]+[\s\S]*?(?:\[\/(?:PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT)\]+|$)/gi, "") .replace(/\[+PREVIEW:[\w-]+:?[\w-]+?\]+[\s\S]*?(?:\[\/(?:PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT)\]+|$)/gi, "")
// Hide closing tags // Hide closing tags
@@ -477,7 +478,7 @@ function parsePlanFromResponse(text: string): { plan: Record<string, any> | null
// --- Main Component --- // --- Main Component ---
export default function AIAssist() { export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } = {}) {
const { const {
language, language,
aiAssistTabs, aiAssistTabs,
@@ -496,6 +497,8 @@ export default function AIAssist() {
} = useStore(); } = useStore();
const t = translations[language].aiAssist; const t = translations[language].aiAssist;
const common = translations[language].common; const common = translations[language].common;
// Vibe mode: override labels
const _vibe = vibeMode ? { studioTitle: "Vibe Architect", studioDesc: "Describe your vision. Get plans, code, and live previews." } : {};
const activeTab = aiAssistTabs?.find(tab => tab.id === activeTabId) || aiAssistTabs?.[0] || { const activeTab = aiAssistTabs?.find(tab => tab.id === activeTabId) || aiAssistTabs?.[0] || {
id: 'default', id: 'default',
@@ -517,6 +520,8 @@ export default function AIAssist() {
const [viewMode, setViewMode] = useState<"preview" | "code">("preview"); const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
const [deviceSize, setDeviceSize] = useState<"full" | "desktop" | "tablet" | "mobile">("full"); const [deviceSize, setDeviceSize] = useState<"full" | "desktop" | "tablet" | "mobile">("full");
const deviceWidths: Record<string, string> = { full: "100%", desktop: "1280px", tablet: "768px", mobile: "375px" }; const deviceWidths: Record<string, string> = { full: "100%", desktop: "1280px", tablet: "768px", mobile: "375px" };
const [isModifying, setIsModifying] = useState(false);
const [seoAuditData, setSeoAuditData] = useState<any>(null);
const [abortController, setAbortController] = useState<AbortController | null>(null); const [abortController, setAbortController] = useState<AbortController | null>(null);
// Agent suggestion state // Agent suggestion state
@@ -574,7 +579,7 @@ export default function AIAssist() {
/module\.exports/i.test(preview.data); /module\.exports/i.test(preview.data);
// Client-side detection // Client-side detection
const isUI = ["web", "app", "design", "html", "ui"].includes(preview.type); const isUI = ["web", "app", "design", "html", "ui", "leads"].includes(preview.type);
const hasTags = /<[a-z][\s\S]*>/i.test(preview.data); const hasTags = /<[a-z][\s\S]*>/i.test(preview.data);
return (isUI || hasTags || preview.language === "html") && !isBackend; return (isUI || hasTags || preview.language === "html") && !isBackend;
@@ -637,10 +642,15 @@ export default function AIAssist() {
setInput(""); setInput("");
} }
// Capture whether this is the initial plan phase (before any code generation) // Plan-first workflow only applies to the "code" agent (software architect)
const wasIdle = !isApproval && (assistStep === "idle" || assistStep === "plan"); // SEO, content, smm, design, web, app agents go straight to preview
const isCodeAgent = currentAgent === "code";
const wasIdle = !isApproval && isCodeAgent && (assistStep === "idle" || assistStep === "plan");
// Detect if user is modifying an existing visual artifact (not text-only agents like SEO/content)
const isVisualAgent = currentAgent === "code" || currentAgent === "web" || currentAgent === "app" || currentAgent === "design" || currentAgent === "general" || currentAgent === "leads";
if (assistStep === "preview" && isVisualAgent) setIsModifying(true);
setIsProcessing(true); setIsProcessing(true);
if (assistStep === "idle") setAssistStep("plan"); if (assistStep === "idle" && isCodeAgent) setAssistStep("plan");
const assistantMsg: AIAssistMessage = { const assistantMsg: AIAssistMessage = {
role: "assistant", role: "assistant",
@@ -692,6 +702,120 @@ export default function AIAssist() {
setStatus(null); setStatus(null);
} }
// 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);
if (uniqueUrls.length > 0) {
setStatus("Auditing website" + (uniqueUrls.length > 1 ? "s" : "") + "...");
try {
for (const url of uniqueUrls) {
const auditRes = await fetch("/api/fetch-url?url=" + encodeURIComponent(url));
if (auditRes.ok) {
const d = await auditRes.json();
setSeoAuditData(d);
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 += "No issues detected.\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 {
setStatus("Searching for SEO context...");
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.results && searchData.results.length > 0) {
const searchContext = searchData.results.slice(0, 5).map((r: { title: string; url: string; snippet: string }, i: number) =>
(i + 1) + ". **" + r.title + "** (" + r.url + ") - " + r.snippet
).join("\n");
enrichedInput = "[WEB SEARCH CONTEXT - Top 5 relevant results]\n" + searchContext + "\n\n---\nUsing the above search results as reference context, answer the user query. Cite sources when relevant.\n\nUser query: " + finalInput;
}
}
} catch (e) { console.warn("SEO web search failed:", e); }
setStatus(null);
}
}
// Leads mode: auto-search for leads
if (currentAgent === "leads" && !webSearchEnabled) {
try {
setStatus("Finding leads...");
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.results && searchData.results.length > 0) {
const searchContext = searchData.results.slice(0, 5).map((r: { title: string; url: string; snippet: string }, i: number) =>
(i + 1) + ". **" + r.title + "** (" + r.url + ") - " + r.snippet
).join("\n");
enrichedInput = "[WEB SEARCH CONTEXT - Use these results to find leads]\n" + searchContext + "\n\n---\nExtract leads/prospects from the above results. Search for more leads using [WEB_SEARCH:query] with different angles. Then output results as a [PREVIEW:leads:html] table.\n\nUser request: " + finalInput;
}
}
} catch (e) { console.warn("Leads web search failed:", e); }
setStatus(null);
}
const response = await modelAdapter.generateAIAssistStream( const response = await modelAdapter.generateAIAssistStream(
{ {
messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }], messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }],
@@ -750,8 +874,9 @@ export default function AIAssist() {
if (!response.success) throw new Error(response.error); 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) { if (wasIdle) {
// Code agent: show plan card for approval
setAssistStep("plan"); setAssistStep("plan");
const { plan: parsedPlan } = parsePlanFromResponse(accumulated); const { plan: parsedPlan } = parsePlanFromResponse(accumulated);
if (parsedPlan) { if (parsedPlan) {
@@ -760,21 +885,34 @@ export default function AIAssist() {
setAiPlan({ rawText: accumulated, architecture: "", techStack: [], files: [], steps: [] }); setAiPlan({ rawText: accumulated, architecture: "", techStack: [], files: [], steps: [] });
} }
} else if ((lastParsedPreview as PreviewData | null)?.data) { } 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"); setAssistStep("preview");
setShowCanvas(true); setShowCanvas(true);
if (isPreviewRenderable(lastParsedPreview)) setViewMode("preview"); if (isPreviewRenderable(lastParsedPreview)) setViewMode("preview");
} else if (!isCodeAgent && !wasIdle) {
// Non-code agent follow-up without new preview:
// Keep existing canvas if we had one, otherwise go idle
if (previewData?.data) {
setAssistStep("preview");
} else {
setAssistStep("idle");
}
} else { } else {
setAssistStep("idle"); setAssistStep("idle");
} }
} catch (error) { } catch (error) {
console.error("Assist error:", error); console.error("Assist error:", error);
const message = error instanceof Error ? error.message : "AI Assist failed"; const rawMessage = error instanceof Error ? error.message : "Vibe Architect failed";
let message = rawMessage;
if (rawMessage.includes("429") || rawMessage.includes("insufficient_quota") || rawMessage.includes("Free allocated quota exceeded") || rawMessage.includes("rate_limit")) {
message = "API quota exceeded or rate limited. Please switch to a different AI provider in Settings, or wait a few minutes and try again.";
}
const errorMsg: AIAssistMessage = { role: "assistant", content: message, timestamp: new Date() }; const errorMsg: AIAssistMessage = { role: "assistant", content: message, timestamp: new Date() };
updateTabById(requestTabId, { history: [...aiAssistHistory, errorMsg] }); updateTabById(requestTabId, { history: [...aiAssistHistory, errorMsg] });
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
setIsModifying(false);
setAbortController(null); setAbortController(null);
setStatus(null); setStatus(null);
} }
@@ -798,6 +936,55 @@ export default function AIAssist() {
setIsProcessing(false); setIsProcessing(false);
} }
}; };
const exportSeoReport = (format: "html" | "pdf") => {
if (!seoAuditData) return;
downloadSeoReport(seoAuditData, format);
};
const exportLeadsReport = (format: "html" | "csv") => {
const htmlContent = previewData?.data;
if (!htmlContent) return;
if (format === "html") {
const blob = new Blob([htmlContent], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "leads-report.html";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} else {
// Extract table rows from HTML and convert to CSV
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, "text/html");
const rows = doc.querySelectorAll("tbody tr");
if (rows.length === 0) return;
let csv = "Name,Platform,Followers,Region,Bio,Link\n";
rows.forEach((row) => {
const cells = row.querySelectorAll("td");
if (cells.length >= 7) {
const name = cells[1]?.textContent?.trim().replace(/,/g, ";") || "";
const platform = cells[2]?.textContent?.trim() || "";
const followers = cells[3]?.textContent?.trim() || "";
const region = cells[4]?.textContent?.trim().replace(/,/g, ";") || "";
const bio = cells[5]?.textContent?.trim().replace(/,/g, ";") || "";
const link = cells[6]?.querySelector("a")?.href || "";
csv += `"${name}","${platform}","${followers}","${region}","${bio}","${link}"\n`;
}
});
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "leads-report.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
};
const clearHistory = () => { const clearHistory = () => {
updateActiveTab({ updateActiveTab({
@@ -989,6 +1176,7 @@ export default function AIAssist() {
{ label: t.agents.seo, agent: "seo", icon: <Search className="h-3.5 w-3.5" /> }, { label: t.agents.seo, agent: "seo", icon: <Search className="h-3.5 w-3.5" /> },
{ label: t.agents.web, agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> }, { label: t.agents.web, agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> },
{ label: t.agents.app, agent: "app", icon: <Play className="h-3.5 w-3.5" /> }, { label: t.agents.app, agent: "app", icon: <Play className="h-3.5 w-3.5" /> },
{ label: t.agents.leads, agent: "leads", icon: <Users className="h-3.5 w-3.5" /> },
].map(({ label, agent, icon }) => ( ].map(({ label, agent, icon }) => (
<button <button
key={agent} key={agent}
@@ -1054,7 +1242,7 @@ export default function AIAssist() {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h4 className="text-sm font-black text-amber-600 dark:text-amber-400">Qwen Authentication Required</h4> <h4 className="text-sm font-black text-amber-600 dark:text-amber-400">Qwen Authentication Required</h4>
<p className="text-xs text-amber-700/70 dark:text-amber-300/70 mt-0.5">Sign in with Qwen to use AI Assist with this provider</p> <p className="text-xs text-amber-700/70 dark:text-amber-300/70 mt-0.5">Sign in with Qwen to use Vibe Architect with this provider</p>
{qwenAuthError && ( {qwenAuthError && (
<p className="text-xs text-red-500 mt-1">{qwenAuthError}</p> <p className="text-xs text-red-500 mt-1">{qwenAuthError}</p>
)} )}
@@ -1070,7 +1258,42 @@ export default function AIAssist() {
</div> </div>
)} )}
{aiAssistHistory.length === 0 && ( {aiAssistHistory.length === 0 && currentAgent === "leads" && (
<div className="h-full flex flex-col items-center justify-center text-center py-12 animate-in zoom-in-95 duration-500">
<div className="p-6 bg-emerald-500/10 rounded-full mb-6 relative">
<Users className="h-16 w-16 text-emerald-400/60" />
<div className="absolute inset-0 bg-emerald-500/10 blur-3xl rounded-full" />
</div>
<h3 className="text-2xl font-black text-slate-900 dark:text-emerald-50 mb-2 tracking-tighter">Leads Finder</h3>
<p className="max-w-md text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed mb-6">
Find influencers, prospects, and leads across social media platforms. Tell me who you are looking for and I will search the web for relevant leads.
</p>
<div className="max-w-sm w-full bg-[#0b1414]/60 dark:bg-slate-800/50 rounded-2xl p-5 border border-emerald-500/20 mb-6 text-left">
<p className="text-[10px] font-black text-emerald-400 uppercase tracking-widest mb-3">To get started, answer these questions:</p>
<div className="space-y-2 text-[13px] text-slate-400 dark:text-slate-300">
<p>1. What <span className="text-emerald-300 font-bold">niche/industry</span> are you targeting? (e.g. forex, SaaS, fitness, crypto)</p>
<p>2. Which <span className="text-emerald-300 font-bold">region or location</span>? (e.g. Singapore, UAE, Global, Latin America)</p>
<p>3. What <span className="text-emerald-300 font-bold">platform</span> preference? (e.g. Instagram, Twitter/X, LinkedIn, YouTube, TikTok, or all)</p>
<p>4. How many leads do you need? (default: 20+)</p>
</div>
</div>
<div className="flex flex-wrap justify-center gap-2">
{["Forex traders in Singapore", "SaaS founders in UAE", "Fitness influencers on Instagram", "Crypto YouTubers in Latin America", "Marketing agencies in Southeast Asia"].map((label: string) => (
<div
key={label}
className="px-3 py-1.5 rounded-full cursor-pointer hover:bg-emerald-600 hover:text-white transition-all text-[10px] font-bold border border-black/20 text-black bg-white/90 shadow-sm"
onClick={() => setInput(label)}
>
{label}
</div>
))}
</div>
</div>
)}
{aiAssistHistory.length === 0 && currentAgent !== "leads" && (
<div className="h-full flex flex-col items-center justify-center text-center py-20 animate-in zoom-in-95 duration-500"> <div className="h-full flex flex-col items-center justify-center text-center py-20 animate-in zoom-in-95 duration-500">
<div className="p-8 bg-blue-500/5 dark:bg-blue-500/10 rounded-full mb-8 relative"> <div className="p-8 bg-blue-500/5 dark:bg-blue-500/10 rounded-full mb-8 relative">
<Ghost className="h-20 w-20 text-blue-400/40 animate-bounce duration-[3s]" /> <Ghost className="h-20 w-20 text-blue-400/40 animate-bounce duration-[3s]" />
@@ -1082,9 +1305,8 @@ export default function AIAssist() {
</p> </p>
<div className="mt-10 flex flex-wrap justify-center gap-3"> <div className="mt-10 flex flex-wrap justify-center gap-3">
{t.suggestions.map((chip: any) => ( {t.suggestions.map((chip: any) => (
<Badge <div
key={chip.label} key={chip.label}
variant="secondary"
className="px-4 py-2 rounded-full cursor-pointer hover:bg-blue-600 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm" className="px-4 py-2 rounded-full cursor-pointer hover:bg-blue-600 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm"
onClick={() => { onClick={() => {
setCurrentAgent(chip.agent); setCurrentAgent(chip.agent);
@@ -1092,7 +1314,7 @@ export default function AIAssist() {
}} }}
> >
{chip.label} {chip.label}
</Badge> </div>
))} ))}
</div> </div>
</div> </div>
@@ -1148,7 +1370,7 @@ export default function AIAssist() {
</div> </div>
{/* Agentic Plan Review Card */} {/* 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") && (
<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"> <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"> <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} <LayoutPanelLeft className="h-4 w-4" /> {t.proposedPlan}
@@ -1169,7 +1391,7 @@ export default function AIAssist() {
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.techStack}</p> <p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.techStack}</p>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{aiPlan.techStack?.map((t_stack: string) => ( {aiPlan.techStack?.map((t_stack: string) => (
<Badge key={t_stack} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge> <Badge variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge>
))} ))}
</div> </div>
</div> </div>
@@ -1209,7 +1431,6 @@ export default function AIAssist() {
{msg.role === "assistant" && msg.preview && !(assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing) && ( {msg.role === "assistant" && msg.preview && !(assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing) && (
<Button <Button
variant="secondary"
size="sm" size="sm"
className="mt-5 w-full bg-blue-50 dark:bg-blue-900/30 border border-blue-200/60 dark:border-blue-800 text-blue-700 dark:text-blue-200 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all" className="mt-5 w-full bg-blue-50 dark:bg-blue-900/30 border border-blue-200/60 dark:border-blue-800 text-blue-700 dark:text-blue-200 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all"
onClick={() => { onClick={() => {
@@ -1225,6 +1446,7 @@ export default function AIAssist() {
{/* Post-coding action buttons */} {/* Post-coding action buttons */}
{msg.role === "assistant" && assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing && ( {msg.role === "assistant" && assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing && (
<>
<div className="mt-4 grid grid-cols-3 gap-2 animate-in zoom-in-95 duration-300"> <div className="mt-4 grid grid-cols-3 gap-2 animate-in zoom-in-95 duration-300">
<Button <Button
onClick={() => { setShowCanvas(true); setViewMode(isPreviewRenderable(previewData as PreviewData) ? "preview" : "code"); }} onClick={() => { setShowCanvas(true); setViewMode(isPreviewRenderable(previewData as PreviewData) ? "preview" : "code"); }}
@@ -1248,6 +1470,81 @@ export default function AIAssist() {
<LayoutPanelLeft className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Modify</span> <LayoutPanelLeft className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Modify</span>
</Button> </Button>
</div> </div>
{currentAgent === "seo" && seoAuditData && (
<div className="mt-2 flex gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => exportSeoReport("html")}
variant="outline"
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/20 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
>
<Download className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export HTML</span>
</Button>
<Button
onClick={() => exportSeoReport("pdf")}
variant="outline"
className="flex-1 bg-amber-500/10 hover:bg-amber-500/20 border-amber-500/20 text-amber-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
>
<FileText className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export PDF</span>
</Button>
</div>
)}
{currentAgent === "leads" && previewData?.data && (
<div className="mt-2 flex gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => exportLeadsReport("html")}
variant="outline"
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/20 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
>
<Download className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export HTML</span>
</Button>
<Button
onClick={() => exportLeadsReport("csv")}
variant="outline"
className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/20 text-blue-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
>
<FileText className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export CSV</span>
</Button>
</div>
)}
</>
)}
{/* Inline SEO Export - always visible in chat when SEO data exists */}
{msg.role === "assistant" && msg.agent === "seo" && seoAuditData && (
<div className="mt-3 flex gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => exportSeoReport("html")}
variant="outline"
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/30 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
>
<Download className="h-3 w-3 mr-1.5" /> <span className="truncate">Export HTML</span>
</Button>
<Button
onClick={() => exportSeoReport("pdf")}
variant="outline"
className="flex-1 bg-amber-500/10 hover:bg-amber-500/20 border-amber-500/30 text-amber-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
>
<FileText className="h-3 w-3 mr-1.5" /> <span className="truncate">Export PDF</span>
</Button>
</div>
)}
{/* Inline Leads Export - always visible in chat when leads data exists */}
{msg.role === "assistant" && msg.agent === "leads" && msg.preview?.data && (
<div className="mt-3 flex gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => { setPreviewData(msg.preview as any); setTimeout(() => exportLeadsReport("html"), 100); }}
variant="outline"
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/30 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
>
<Download className="h-3 w-3 mr-1.5" /> <span className="truncate">Export HTML</span>
</Button>
<Button
onClick={() => { setPreviewData(msg.preview as any); setTimeout(() => exportLeadsReport("csv"), 100); }}
variant="outline"
className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30 text-blue-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
>
<FileText className="h-3 w-3 mr-1.5" /> <span className="truncate">Export CSV</span>
</Button>
</div>
)} )}
</div> </div>
@@ -1412,7 +1709,23 @@ export default function AIAssist() {
</div> </div>
<div className="flex-1 overflow-auto relative bg-[#050505]"> <div className="flex-1 overflow-auto relative bg-[#050505]">
{viewMode === "preview" && currentPreviewData ? ( {isModifying ? (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-[#050505]/95 backdrop-blur-xl">
<div className="relative mb-6">
<div className="w-16 h-16 rounded-full border-[3px] border-blue-500/20 border-t-blue-500 animate-spin" />
<div className="absolute inset-0 flex items-center justify-center">
<Wrench className="h-6 w-6 text-blue-400 animate-pulse" />
</div>
</div>
<p className="text-sm font-black text-blue-200 uppercase tracking-[0.2em]">Modification in Progress</p>
<p className="text-[10px] text-blue-400/60 mt-2 font-semibold tracking-wider">Rewriting your artifact...</p>
<div className="flex items-center gap-1.5 mt-4">
{[0, 1, 2].map(i => (
<div key={i} className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: `${i * 0.15}s` }} />
))}
</div>
</div>
) : viewMode === "preview" && currentPreviewData ? (
<CanvasErrorBoundary> <CanvasErrorBoundary>
<div className="mx-auto transition-all duration-300 h-full" <div className="mx-auto transition-all duration-300 h-full"
style={deviceSize !== "full" style={deviceSize !== "full"
@@ -1444,9 +1757,9 @@ export default function AIAssist() {
{currentPreviewData?.isStreaming ? t.neuralLinkActive : t.syncComplete} {currentPreviewData?.isStreaming ? t.neuralLinkActive : t.syncComplete}
</span> </span>
</div> </div>
<Badge variant="outline" className="text-[9px] border-blue-900 text-blue-200/50 font-black"> <div className="text-[9px] border-blue-900 text-blue-200/50 font-black">
{currentPreviewData?.language?.toUpperCase()} UTF-8 {currentPreviewData?.language?.toUpperCase()} UTF-8
</Badge> </div>
</div> </div>
</Card> </Card>
</div> </div>

View File

@@ -20,7 +20,8 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
const t = translations[language].sidebar; const t = translations[language].sidebar;
const common = translations[language].common; const common = translations[language].common;
const menuItems = [ const menuItems: Array<{ id: View; label: string; icon: any; count?: number; highlight?: boolean }> = [
{ id: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare, highlight: true },
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles }, { id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
{ id: "prd" as View, label: t.prdGenerator, icon: FileText }, { id: "prd" as View, label: t.prdGenerator, icon: FileText },
{ id: "action" as View, label: t.actionPlan, icon: ListTodo }, { id: "action" as View, label: t.actionPlan, icon: ListTodo },
@@ -28,7 +29,6 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
{ id: "slides" as View, label: t.slidesGen, icon: Presentation }, { id: "slides" as View, label: t.slidesGen, icon: Presentation },
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone }, { id: "googleads" as View, label: t.googleAds, icon: Megaphone },
{ id: "market-research" as View, label: t.marketResearch, icon: Search }, { id: "market-research" as View, label: t.marketResearch, icon: Search },
{ id: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare },
{ id: "history" as View, label: t.history, icon: History, count: history.length }, { id: "history" as View, label: t.history, icon: History, count: history.length },
{ id: "settings" as View, label: t.settings, icon: Settings }, { id: "settings" as View, label: t.settings, icon: Settings },
]; ];
@@ -69,7 +69,8 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
variant={currentView === item.id ? "default" : "ghost"} variant={currentView === item.id ? "default" : "ghost"}
className={cn( className={cn(
"w-full justify-start gap-2 h-9 lg:h-10 text-sm", "w-full justify-start gap-2 h-9 lg:h-10 text-sm",
currentView === item.id && "bg-primary text-primary-foreground" currentView === item.id && "bg-primary text-primary-foreground",
item.highlight && currentView !== item.id && "bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-500/20 text-blue-600 dark:text-blue-400 font-bold"
)} )}
onClick={() => handleViewChange(item.id)} onClick={() => handleViewChange(item.id)}
> >

View File

@@ -12,7 +12,7 @@ export const translations = {
googleAds: "Google Ads", googleAds: "Google Ads",
uxDesigner: "UX Designer", uxDesigner: "UX Designer",
marketResearch: "Market Research", marketResearch: "Market Research",
aiAssist: "AI Assist", aiAssist: "Vibe Architect",
settings: "Settings", settings: "Settings",
history: "History", history: "History",
backToRommark: "Back to rommark.dev", backToRommark: "Back to rommark.dev",
@@ -388,8 +388,8 @@ export const translations = {
functionalAudit: "Functional Audit", functionalAudit: "Functional Audit",
}, },
aiAssist: { aiAssist: {
title: "AI Assist", title: "Vibe Architect",
description: "Conversational intelligence with agent switching", description: "Plan, code, and ship with AI — visual canvas included",
placeholder: "Discuss any topic, concern or project...", placeholder: "Discuss any topic, concern or project...",
chatStart: "How can I help you today?", chatStart: "How can I help you today?",
switchingAgent: "Switching to specialized agent...", switchingAgent: "Switching to specialized agent...",
@@ -434,8 +434,8 @@ export const translations = {
inspectCode: "Inspect Code", inspectCode: "Inspect Code",
liveRender: "Live Render", liveRender: "Live Render",
canvasTitle: (type: string) => `${type} Canvas`, canvasTitle: (type: string) => `${type} Canvas`,
studioTitle: "Studio-grade AI Assist", studioTitle: "Vibe Architect",
studioDesc: "Switch agents, stream answers, and light up the canvas with live artifacts.", studioDesc: "Describe your vision. Get plans, code, and live previews.",
askArtifact: "Ask for a design, code, or research artifact.", askArtifact: "Ask for a design, code, or research artifact.",
suggestions: [ suggestions: [
{ label: "Build a landing UI", agent: "web" }, { label: "Build a landing UI", agent: "web" },
@@ -448,7 +448,8 @@ export const translations = {
design: "Design", design: "Design",
seo: "SEO", seo: "SEO",
web: "Web", web: "Web",
app: "App" app: "App",
leads: "Leads Finder"
}, },
userLabel: "Explorer", userLabel: "Explorer",
canvasLabel: "Canvas", canvasLabel: "Canvas",
@@ -477,7 +478,7 @@ export const translations = {
googleAds: "Google Реклама", googleAds: "Google Реклама",
uxDesigner: "UX Дизайнер", uxDesigner: "UX Дизайнер",
marketResearch: "Анализ рынка", marketResearch: "Анализ рынка",
aiAssist: "ИИ Ассистент", aiAssist: "Vibe Architect",
settings: "Настройки", settings: "Настройки",
history: "История", history: "История",
backToRommark: "Вернуться на rommark.dev", backToRommark: "Вернуться на rommark.dev",
@@ -853,7 +854,7 @@ export const translations = {
functionalAudit: "Функциональный аудит", functionalAudit: "Функциональный аудит",
}, },
aiAssist: { aiAssist: {
title: "ИИ Ассистент", title: "Vibe Architect",
description: "Диалоговый интеллект с переключением агентов", description: "Диалоговый интеллект с переключением агентов",
placeholder: "Обсудите любую тему, проблему или проект...", placeholder: "Обсудите любую тему, проблему или проект...",
chatStart: "Чем я могу помочь вам сегодня?", chatStart: "Чем я могу помочь вам сегодня?",
@@ -899,7 +900,7 @@ export const translations = {
inspectCode: "Просмотр кода", inspectCode: "Просмотр кода",
liveRender: "Живой рендеринг", liveRender: "Живой рендеринг",
canvasTitle: (type: string) => `Холст: ${type}`, canvasTitle: (type: string) => `Холст: ${type}`,
studioTitle: "ИИ Ассистент студийного уровня", studioTitle: "Vibe Architect студийного уровня",
studioDesc: "Переключайте агентов, получайте ответы и оживляйте холст артефактами.", studioDesc: "Переключайте агентов, получайте ответы и оживляйте холст артефактами.",
askArtifact: "Запросите дизайн, код или исследование.", askArtifact: "Запросите дизайн, код или исследование.",
suggestions: [ suggestions: [
@@ -913,7 +914,8 @@ export const translations = {
design: "Дизайн", design: "Дизайн",
seo: "SEO", seo: "SEO",
web: "Веб", web: "Веб",
app: "Приложение" app: "Приложение",
leads: "Поиск лидов"
}, },
userLabel: "Исследователь", userLabel: "Исследователь",
canvasLabel: "Холст", canvasLabel: "Холст",
@@ -942,7 +944,7 @@ export const translations = {
googleAds: "Google Ads", googleAds: "Google Ads",
uxDesigner: "מעצב UX", uxDesigner: "מעצב UX",
marketResearch: "מחקר שוק", marketResearch: "מחקר שוק",
aiAssist: "סייען AI", aiAssist: "Vibe Architect",
settings: "הגדרות", settings: "הגדרות",
history: "היסטוריה", history: "היסטוריה",
backToRommark: "חזרה ל-rommark.dev", backToRommark: "חזרה ל-rommark.dev",
@@ -1318,7 +1320,7 @@ export const translations = {
functionalAudit: "ביקורת פונקציונלית", functionalAudit: "ביקורת פונקציונלית",
}, },
aiAssist: { aiAssist: {
title: "סייען AI", title: "Vibe Architect",
description: "אינטליגנציה שיחתית עם החלפת סוכנים", description: "אינטליגנציה שיחתית עם החלפת סוכנים",
placeholder: "דון בכל נושא, חשש או פרויקט...", placeholder: "דון בכל נושא, חשש או פרויקט...",
chatStart: "במה אוכל לעזור לך היום?", chatStart: "במה אוכל לעזור לך היום?",
@@ -1364,7 +1366,7 @@ export const translations = {
inspectCode: "בדוק קוד", inspectCode: "בדוק קוד",
liveRender: "רינדור חי", liveRender: "רינדור חי",
canvasTitle: (type: string) => `קנבס ${type}`, canvasTitle: (type: string) => `קנבס ${type}`,
studioTitle: "סייען AI ברמה מקצועית", studioTitle: "Vibe Architect ברמה מקצועית",
studioDesc: "החלף סוכנים, הזרם תשובות והפעל את הקנבס עם ארטיפקטים חיים.", studioDesc: "החלף סוכנים, הזרם תשובות והפעל את הקנבס עם ארטיפקטים חיים.",
askArtifact: "בקש עיצוב, קוד או מחקר.", askArtifact: "בקש עיצוב, קוד או מחקר.",
suggestions: [ suggestions: [
@@ -1378,7 +1380,8 @@ export const translations = {
design: "עיצוב", design: "עיצוב",
seo: "SEO", seo: "SEO",
web: "אינטרנט", web: "אינטרנט",
app: "אפליקציה" app: "אפליקציה",
leads: "חיפוש לידים"
}, },
userLabel: "סייר", userLabel: "סייר",
canvasLabel: "קנבס", canvasLabel: "קנבס",

573
lib/seo-report.ts Normal file
View File

@@ -0,0 +1,573 @@
/**
* SEO/GEO Audit Report Generator v2
* Comprehensive standalone HTML reports with all audit sections
*/
export interface SeoAuditData {
url: string;
domain: string;
protocol?: string;
responseTime: number;
server: string;
htmlSize: number;
title: string | null;
titleLength: number;
titleStatus: string;
metaDescription: string | null;
descLength: number;
descStatus: string;
metaKeywords: string | null;
viewport: string | null;
charset: string | null;
robotsDirectives: string | null;
canonical: string | null;
hasCanonicalMismatch: boolean;
xFrameOptions?: string;
h1Count: number;
h2Count: number;
h3Count: number;
h4Count: number;
headingStatus: string;
headings: { level: number; text: string }[];
links?: {
total: number;
internal: number;
external: number;
nofollow: number;
sampleExternal?: { href: string; text: string; nofollow: boolean }[];
};
images?: {
total: number;
withAlt: number;
withoutAlt: number;
lazyLoaded: number;
altCoverage: number;
sampleWithoutAlt?: string[];
};
content?: {
wordCount: number;
sentenceCount: number;
paragraphCount: number;
avgWordsPerSentence: number;
textPreview?: string;
};
openGraph?: { title: string | null; description: string | null; image: string | null; type: string | null; url?: string | null };
twitterCard?: { card: string | null; title?: string | null; description?: string | null };
hreflang?: string[];
performance?: {
inlineStyles: number;
inlineScripts?: number;
externalScripts: number;
externalStylesheets: number;
hasPreconnect: boolean;
hasPreload: boolean;
hasDnsPrefetch: boolean;
usesAsyncScripts: boolean;
usesDeferScripts: boolean;
contentEncoding?: string;
};
accessibility?: { hasLangAttr: boolean; hasAriaLabels: boolean; hasAltOnFirstImage: boolean };
structuredData?: { hasJsonLd: boolean; hasMicrodata: boolean; types: { type: string; found: boolean }[] };
scores?: { overall: number; technical: number; content: number; performance: number; social: number };
issues?: { severity: string; category: string; message: string }[];
}
const sc = (s: number): string =>
s >= 80 ? "#22c55e" : s >= 60 ? "#f59e0b" : "#ef4444";
const svc = (s: string): string =>
s === "critical" ? "#ef4444" : s === "warning" ? "#f59e0b" : "#6b7280";
const badge = (label: string, color: string): string =>
'<span style="display:inline-block;padding:2px 10px;border-radius:6px;font-size:11px;font-weight:700;color:white;background:' + color + '">' + label + '</span>';
const passFail = (val: boolean | null | undefined, yesLabel?: string, noLabel?: string): string =>
val ? badge(yesLabel || "PASS", "#22c55e") : badge(noLabel || "FAIL", "#ef4444");
const statusBadge = (status: string): string => {
if (status === "good" || status === "ok" || status === "pass") return badge("GOOD", "#22c55e");
if (status === "missing" || status === "fail") return badge("MISSING", "#ef4444");
if (status === "too_long" || status === "warning" || status === "multiple_h1") return badge("WARNING", "#f59e0b");
if (status === "mismatch" || status === "missing_h1") return badge("CRITICAL", "#ef4444");
return badge(status.toUpperCase(), "#6b7280");
};
const t = (v: string | null | undefined, fallback: string): string =>
v ? v : '<em style="color:#64748b">' + fallback + '</em>';
const fixMap: Record<string, string> = {
Meta: "Ensure every page has a unique title tag (50-60 chars) with primary keyword near start. Write meta descriptions (150-160 chars) with a clear CTA. Add viewport meta tag for mobile compatibility. Include target keywords naturally without stuffing.",
Content: "Maintain exactly one H1 per page containing the primary keyword. Build logical heading hierarchy (H1 > H2 > H3). Aim for 1000+ words on core pages. Ensure each paragraph adds value. Use short paragraphs (3-4 sentences) for readability.",
Technical: "Set self-referencing canonical tags on all pages. Ensure valid SSL certificate with proper HTTPS redirect. Check robots.txt is not blocking important pages. Fix redirect chains (max 2 hops). Implement proper 301 redirects for moved pages.",
Mobile: "Add viewport meta tag. Test with Google Mobile-Friendly test. Ensure tap targets are minimum 48x48px. Avoid horizontal scroll. Use responsive images with srcset.",
Security: "Migrate to HTTPS with valid SSL certificate. Set up HTTP-to-HTTPS redirects. Configure HSTS headers. Set X-Frame-Options to prevent clickjacking. Implement Content-Security-Policy headers.",
Performance: "Minimize inline styles. Add preconnect hints for third-party domains. Implement lazy loading for images. Use async/defer for non-critical scripts. Compress images (WebP/AVIF). Enable Brotli/gzip compression. Minimize render-blocking resources.",
Social: "Add Open Graph tags (og:title, og:description, og:image, og:url, og:type). Implement Twitter Card meta tags. Ensure OG images are 1200x630px minimum. Validate with Facebook Sharing Debugger and Twitter Card Validator.",
Accessibility: "Add lang attribute to <html> tag. Implement ARIA labels for interactive elements. Add descriptive alt text to all images. Ensure keyboard navigation works. Use sufficient color contrast ratios (WCAG AA minimum).",
Links: "Fix or remove broken internal links. Add external links to authoritative sources. Review nofollow attributes on external links. Ensure descriptive anchor text (avoid 'click here'). Implement logical internal linking structure.",
"Structured Data": "Implement JSON-LD structured data for relevant content types. Validate with Google Rich Results Test. Add FAQ, Article, Product, or Organization schema as appropriate. Use schema.org markup for entities and facts.",
};
export function generateSeoReportHtml(d: SeoAuditData): string {
const now = new Date().toLocaleString();
// --- Issue Rows ---
const criticalIssues = (d.issues || []).filter(i => i.severity === "critical");
const warningIssues = (d.issues || []).filter(i => i.severity === "warning");
const infoIssues = (d.issues || []).filter(i => i.severity === "info");
const issueRow = (issue: { severity: string; category: string; message: string }): string =>
"<tr><td>" + badge(issue.severity.toUpperCase(), svc(issue.severity)) + "</td><td style=\"font-weight:600\">" + issue.category + "</td><td>" + issue.message + "</td><td style=\"white-space:pre-wrap;font-size:12px;color:#22c55e\">" + (fixMap[issue.category] || "Review and address this issue based on SEO best practices.") + "</td></tr>";
const allIssueRows = (d.issues || []).map(issueRow).join("");
// --- Heading Rows ---
const headingRows = (d.headings || []).slice(0, 30).map((h) =>
"<tr><td style=\"font-weight:700;padding:4px 12px\"><span style=\"display:inline-block;padding:1px 8px;border-radius:4px;font-size:10px;background:#334155\">" + h.level + "</span></td><td>" + h.text + "</td></tr>"
).join("");
// --- Structured Data Rows ---
const sdTypes = d.structuredData?.types || [];
const sdRows = sdTypes.map((s) =>
"<tr><td>" + s.type + "</td><td>" + passFail(s.found, "FOUND", "NOT FOUND") + "</td></tr>"
).join("");
// --- External Link Rows ---
const extLinks = d.links?.sampleExternal || [];
const extLinkRows = extLinks.slice(0, 15).map((l) =>
"<tr><td style=\"max-width:300px;word-break:break-all\"><a href=\"" + l.href + "\" target=\"_blank\">" + l.href + "</a></td><td>" + (l.text || "N/A") + "</td><td>" + (l.nofollow ? badge("NOFOLLOW", "#f59e0b") : badge("FOLLOW", "#22c55e")) + "</td></tr>"
).join("");
// --- Missing Alt Image Rows ---
const missingAlts = d.images?.sampleWithoutAlt || [];
const altRows = missingAlts.slice(0, 10).map((src) =>
"<tr><td style=\"max-width:500px;word-break:break-all;font-size:11px\">" + src + "</td></tr>"
).join("");
// --- Hreflang Rows ---
const hreflangTags = d.hreflang || [];
const hreflangRows = hreflangTags.map((h) =>
"<tr><td>" + h + "</td></tr>"
).join("");
// --- GEO Score Calculation ---
const geoScore = calculateGeoScore(d);
const geoColor = sc(geoScore);
// --- Action Plan ---
const actionPlan = generateActionPlan(d);
// --- Build HTML ---
const css = "*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6;padding:40px}@media print{body{background:#fff;color:#1e293b;padding:20px}}.container{max-width:960px;margin:0 auto}@media print{.container{max-width:100%}}.hero{text-align:center;padding:32px;background:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);border-radius:20px;margin-bottom:32px;border:1px solid #334155}@media print{.hero{background:#f8fafc;border:1px solid #e2e8f0}}.hero h1{font-size:30px;margin-bottom:4px}.hero .url{color:#60a5fa;font-family:monospace;font-size:13px;word-break:break-all}.hero .meta{color:#94a3b8;margin-top:8px;font-size:12px}.scores{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin:24px 0}.score-card{text-align:center;padding:18px 8px;background:#1e293b;border-radius:14px;border:1px solid #334155}@media print{.score-card{background:#f8fafc;border:1px solid #e2e8f0}}.score-value{font-size:34px;font-weight:800}.score-label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;margin-top:4px}table{width:100%;border-collapse:collapse;margin:14px 0;background:#1e293b;border-radius:14px;overflow:hidden;border:1px solid #334155}@media print{table{background:#f8fafc;border:1px solid #e2e8f0}}th,td{padding:10px 14px;text-align:left;border-bottom:1px solid #334155;font-size:13px}@media print{th,td{border-bottom:1px solid #e2e8f0}}th{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;background:#162032}@media print{th{background:#f1f5f9;color:#64748b}}tr:hover{background:#1a2744}@media print{tr:hover{background:#f8fafc}}a{color:#60a5fa}.section{background:#1e293b;border-radius:16px;padding:24px;margin-bottom:20px;border:1px solid #334155}@media print{.section{background:#f8fafc;border:1px solid #e2e8f0}}h2{font-size:18px;margin-bottom:14px;padding-bottom:8px;border-bottom:2px solid #334155;display:flex;align-items:center;gap:8px}h2 .icon{font-size:20px}.stat-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin:12px 0}.stat-item{background:#162032;padding:12px;border-radius:10px;text-align:center}@media print{.stat-item{background:#f1f5f9}}.stat-value{font-size:22px;font-weight:800}.stat-label{font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-top:2px}.action-item{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid #334155}.action-item:last-child{border:none}.action-priority{font-size:10px;font-weight:800;padding:2px 8px;border-radius:4px;text-transform:uppercase;white-space:nowrap}.action-priority.high{background:#ef444420;color:#ef4444}.action-priority.medium{background:#f59e0b20;color:#f59e0b}.action-priority.low{background:#22c55e20;color:#22c55e}.geo-bar{height:8px;background:#334155;border-radius:4px;overflow:hidden;margin:8px 0}.geo-fill{height:100%;border-radius:4px;transition:width 0.3s}.footer{text-align:center;padding:32px;color:#64748b;font-size:12px}";
let html = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>SEO/GEO Audit - " +
(d.domain || "report") +
"</title><style>" + css + "</style></head><body><div class=\"container\">";
// === HERO ===
html += "<div class=\"hero\"><h1>Comprehensive SEO/GEO Audit Report</h1>" +
"<p class=\"url\">" + (d.url || "N/A") + "</p>" +
"<p class=\"meta\">Generated by PromptArch Vibe Architect | " + now + "</p></div>";
// === EXECUTIVE SUMMARY ===
html += "<div class=\"section\"><h2><span class=\"icon\">01</span> Executive Summary</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\" style=\"color:" + sc(d.scores?.overall || 0) + "\">" + (d.scores?.overall || 0) + "/100</div><div class=\"stat-label\">Overall Score</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.issues?.length || 0) + "</div><div class=\"stat-label\">Issues Found</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + criticalIssues.length + "</div><div class=\"stat-label\">Critical Issues</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + geoScore + "/100</div><div class=\"stat-label\">GEO Readiness</div></div>" +
"</div></div>";
// === SCORES ===
html += "<div class=\"section\"><h2><span class=\"icon\">02</span> Scoring Breakdown</h2>" +
"<div class=\"scores\">" +
scoreCard("Overall", d.scores?.overall || 0) +
scoreCard("Technical", d.scores?.technical || 0) +
scoreCard("Content", d.scores?.content || 0) +
scoreCard("Performance", d.scores?.performance || 0) +
scoreCard("Social", d.scores?.social || 0) +
"<div class=\"score-card\"><div class=\"score-value\" style=\"color:" + geoColor + "\">" + geoScore + "</div><div class=\"score-label\">GEO</div></div>" +
"</div>" +
geoBar("Technical SEO", d.scores?.technical || 0) +
geoBar("Content Quality", d.scores?.content || 0) +
geoBar("Performance", d.scores?.performance || 0) +
geoBar("Social/OG", d.scores?.social || 0) +
geoBar("GEO Readiness", geoScore) +
"</div>";
// === META TAGS ===
html += "<div class=\"section\"><h2><span class=\"icon\">03</span> Meta Tags Analysis</h2>" +
"<table><tr><th>Element</th><th>Value</th><th>Status</th></tr>" +
"<tr><td><strong>Title</strong></td><td>" + t(d.title, "MISSING") + " <span style=\"color:#64748b\">(" + (d.titleLength || 0) + " chars)</span></td><td>" + statusBadge(d.titleStatus) + "</td></tr>" +
"<tr><td><strong>Meta Description</strong></td><td>" + t(d.metaDescription, "MISSING") + " <span style=\"color:#64748b\">(" + (d.descLength || 0) + " chars)</span></td><td>" + statusBadge(d.descStatus) + "</td></tr>" +
"<tr><td><strong>Meta Keywords</strong></td><td>" + t(d.metaKeywords, "Not set (modern SEO does not require)") + "</td><td><span style=\"color:#64748b;font-size:11px\">Not a ranking factor</span></td></tr>" +
"<tr><td><strong>Viewport</strong></td><td>" + t(d.viewport, "MISSING") + "</td><td>" + passFail(!!d.viewport) + "</td></tr>" +
"<tr><td><strong>Charset</strong></td><td>" + t(d.charset, "MISSING") + "</td><td>" + passFail(!!d.charset) + "</td></tr>" +
"<tr><td><strong>Canonical</strong></td><td>" + (d.canonical || "<em>None</em>") + "</td><td>" + (d.hasCanonicalMismatch ? badge("MISMATCH", "#ef4444") : d.canonical ? badge("OK", "#22c55e") : badge("MISSING", "#ef4444")) + "</td></tr>" +
"<tr><td><strong>Robots</strong></td><td>" + (d.robotsDirectives || "None") + "</td><td><span style=\"color:#64748b;font-size:11px\">-</span></td></tr>" +
"<tr><td><strong>X-Frame-Options</strong></td><td>" + t(d.xFrameOptions, "Not set") + "</td><td>" + passFail(!!d.xFrameOptions) + "</td></tr>" +
"<tr><td><strong>Protocol</strong></td><td>" + (d.protocol || "Unknown") + "</td><td>" + (d.protocol === "HTTPS" ? badge("SECURE", "#22c55e") : badge("INSECURE", "#ef4444")) + "</td></tr>" +
"</table></div>";
// === SOCIAL / OPEN GRAPH ===
html += "<div class=\"section\"><h2><span class=\"icon\">04</span> Social & Open Graph</h2>" +
"<table><tr><th>Property</th><th>Value</th><th>Status</th></tr>" +
"<tr><td><strong>OG Title</strong></td><td>" + t(d.openGraph?.title, "Missing") + "</td><td>" + passFail(!!d.openGraph?.title) + "</td></tr>" +
"<tr><td><strong>OG Description</strong></td><td>" + t(d.openGraph?.description, "Missing") + "</td><td>" + passFail(!!d.openGraph?.description) + "</td></tr>" +
"<tr><td><strong>OG Image</strong></td><td>" + (d.openGraph?.image ? '<a href="' + d.openGraph.image + '" target="_blank" style="font-size:12px">' + d.openGraph.image.substring(0, 80) + '...</a>' : "<em>Missing</em>") + "</td><td>" + passFail(!!d.openGraph?.image) + "</td></tr>" +
"<tr><td><strong>OG Type</strong></td><td>" + t(d.openGraph?.type, "Missing") + "</td><td>" + passFail(!!d.openGraph?.type) + "</td></tr>" +
"<tr><td><strong>OG URL</strong></td><td>" + t(d.openGraph?.url, "Missing") + "</td><td>" + passFail(!!d.openGraph?.url) + "</td></tr>" +
"<tr><td><strong>Twitter Card</strong></td><td>" + t(d.twitterCard?.card, "Not set") + "</td><td>" + passFail(!!d.twitterCard?.card) + "</td></tr>" +
"<tr><td><strong>Twitter Title</strong></td><td>" + t(d.twitterCard?.title, "Missing") + "</td><td>" + passFail(!!d.twitterCard?.title) + "</td></tr>" +
"<tr><td><strong>Twitter Description</strong></td><td>" + t(d.twitterCard?.description, "Missing") + "</td><td>" + passFail(!!d.twitterCard?.description) + "</td></tr>" +
"</table></div>";
// === HEADINGS ===
html += "<div class=\"section\"><h2><span class=\"icon\">05</span> Heading Structure</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h1Count + "</div><div class=\"stat-label\">H1</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h2Count + "</div><div class=\"stat-label\">H2</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h3Count + "</div><div class=\"stat-label\">H3</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h4Count + "</div><div class=\"stat-label\">H4+</div></div>" +
"</div>" +
"<p style=\"margin:10px 0;color:#94a3b8\">Status: " + statusBadge(d.headingStatus) + "</p>" +
(headingRows ? "<p style=\"color:#94a3b8;font-size:12px;margin-bottom:8px\">Heading hierarchy (up to 30 shown):</p><table><tr><th style=\"width:60px\">Level</th><th>Text</th></tr>" + headingRows + "</table>" : "<p style=\"color:#64748b\">No headings found.</p>") +
"</div>";
// === LINKS ===
html += "<div class=\"section\"><h2><span class=\"icon\">06</span> Links Analysis</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.total || 0) + "</div><div class=\"stat-label\">Total Links</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.internal || 0) + "</div><div class=\"stat-label\">Internal</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.external || 0) + "</div><div class=\"stat-label\">External</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.nofollow || 0) + "</div><div class=\"stat-label\">Nofollow</div></div>" +
"</div>";
if (extLinkRows) {
html += "<p style=\"color:#94a3b8;font-size:12px;margin-bottom:8px\">Sample external links:</p>" +
"<table><tr><th>URL</th><th>Anchor Text</th><th>Rel</th></tr>" + extLinkRows + "</table>";
}
html += "</div>";
// === IMAGES ===
html += "<div class=\"section\"><h2><span class=\"icon\">07</span> Images & Alt Text</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.total || 0) + "</div><div class=\"stat-label\">Total Images</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.withAlt || 0) + "</div><div class=\"stat-label\">With Alt</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.withoutAlt || 0) + "</div><div class=\"stat-label\">Missing Alt</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.altCoverage || 0) + "%</div><div class=\"stat-label\">Alt Coverage</div></div>" +
"</div>" +
"<p style=\"margin:10px 0\">Lazy Loaded: " + passFail((d.images?.lazyLoaded || 0) > 0) + " (" + (d.images?.lazyLoaded || 0) + " images)</p>";
if (altRows) {
html += "<p style=\"color:#f59e0b;font-size:12px;margin-bottom:8px\">Images missing alt text:</p>" +
"<table><tr><th>Image Source</th></tr>" + altRows + "</table>";
}
html += "</div>";
// === CONTENT ===
html += "<div class=\"section\"><h2><span class=\"icon\">08</span> Content Analysis</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.wordCount || 0) + "</div><div class=\"stat-label\">Word Count</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.sentenceCount || 0) + "</div><div class=\"stat-label\">Sentences</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.paragraphCount || 0) + "</div><div class=\"stat-label\">Paragraphs</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.avgWordsPerSentence || 0) + "</div><div class=\"stat-label\">Avg Words/Sentence</div></div>" +
"</div>" +
"<table><tr><th>Metric</th><th>Value</th><th>Recommendation</th></tr>" +
"<tr><td>Word Count</td><td>" + (d.content?.wordCount || 0) + "</td><td>" + ((d.content?.wordCount || 0) >= 1000 ? badge("GOOD", "#22c55e") : badge("BELOW 1000", "#f59e0b")) + " Aim for 1000+ on core pages</td></tr>" +
"<tr><td>Readability</td><td>" + (d.content?.avgWordsPerSentence || 0) + " avg words/sentence</td><td>" + ((d.content?.avgWordsPerSentence || 0) <= 20 ? badge("GOOD", "#22c55e") : badge("LONG", "#f59e0b")) + " Keep under 20 for readability</td></tr>" +
"</table></div>";
// === PERFORMANCE ===
html += "<div class=\"section\"><h2><span class=\"icon\">09</span> Performance Signals</h2>" +
"<table><tr><th>Metric</th><th>Value</th><th>Status</th></tr>" +
"<tr><td><strong>Server</strong></td><td>" + (d.server || "Unknown") + "</td><td>-</td></tr>" +
"<tr><td><strong>Response Time</strong></td><td>" + d.responseTime + "ms</td><td>" + (d.responseTime < 500 ? badge("FAST", "#22c55e") : d.responseTime < 1500 ? badge("OK", "#f59e0b") : badge("SLOW", "#ef4444")) + "</td></tr>" +
"<tr><td><strong>HTML Size</strong></td><td>" + (d.htmlSize || 0).toLocaleString() + " bytes</td><td>-</td></tr>" +
"<tr><td><strong>Content Encoding</strong></td><td>" + t(d.performance?.contentEncoding, "None detected") + "</td><td>" + passFail(!!d.performance?.contentEncoding) + "</td></tr>" +
"<tr><td><strong>External Scripts</strong></td><td>" + (d.performance?.externalScripts || 0) + "</td><td>" + ((d.performance?.externalScripts || 0) <= 5 ? badge("OK", "#22c55e") : badge("HIGH", "#f59e0b")) + "</td></tr>" +
"<tr><td><strong>External Stylesheets</strong></td><td>" + (d.performance?.externalStylesheets || 0) + "</td><td>-</td></tr>" +
"<tr><td><strong>Inline Styles</strong></td><td>" + (d.performance?.inlineStyles || 0) + "</td><td>" + ((d.performance?.inlineStyles || 0) <= 10 ? badge("OK", "#22c55e") : badge("HIGH", "#f59e0b")) + "</td></tr>" +
"<tr><td><strong>Inline Scripts</strong></td><td>" + (d.performance?.inlineScripts || 0) + "</td><td>" + ((d.performance?.inlineScripts || 0) === 0 ? badge("GOOD", "#22c55e") : badge("FOUND", "#f59e0b")) + "</td></tr>" +
"<tr><td><strong>Preconnect</strong></td><td>" + (d.performance?.hasPreconnect ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.hasPreconnect) + "</td></tr>" +
"<tr><td><strong>Preload</strong></td><td>" + (d.performance?.hasPreload ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.hasPreload) + "</td></tr>" +
"<tr><td><strong>DNS Prefetch</strong></td><td>" + (d.performance?.hasDnsPrefetch ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.hasDnsPrefetch) + "</td></tr>" +
"<tr><td><strong>Async/Defer Scripts</strong></td><td>" + (d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts) + "</td></tr>" +
"</table></div>";
// === ACCESSIBILITY ===
html += "<div class=\"section\"><h2><span class=\"icon\">10</span> Accessibility</h2>" +
"<table><tr><th>Check</th><th>Status</th><th>Impact</th></tr>" +
"<tr><td><strong>HTML lang attribute</strong></td><td>" + passFail(d.accessibility?.hasLangAttr) + "</td><td>Screen readers, SEO</td></tr>" +
"<tr><td><strong>ARIA labels present</strong></td><td>" + passFail(d.accessibility?.hasAriaLabels) + "</td><td>Assistive technology</td></tr>" +
"<tr><td><strong>First image has alt text</strong></td><td>" + passFail(d.accessibility?.hasAltOnFirstImage) + "</td><td>Screen readers</td></tr>" +
"<tr><td><strong>Alt text coverage</strong></td><td>" + (d.images?.altCoverage || 0) + "%</td><td>" + ((d.images?.altCoverage || 0) >= 90 ? badge("GOOD", "#22c55e") : (d.images?.altCoverage || 0) >= 70 ? badge("FAIR", "#f59e0b") : badge("POOR", "#ef4444")) + "</td></tr>" +
"</table></div>";
// === STRUCTURED DATA ===
html += "<div class=\"section\"><h2><span class=\"icon\">11</span> Structured Data / Schema</h2>" +
"<table><tr><th>Format</th><th>Status</th></tr>" +
"<tr><td><strong>JSON-LD</strong></td><td>" + passFail(d.structuredData?.hasJsonLd) + "</td></tr>" +
"<tr><td><strong>Microdata</strong></td><td>" + passFail(d.structuredData?.hasMicrodata) + "</td></tr>" +
"</table>";
if (sdRows) {
html += "<p style=\"color:#94a3b8;font-size:12px;margin:10px 0\">Detected schema types:</p>" +
"<table><tr><th>Schema Type</th><th>Found</th></tr>" + sdRows + "</table>";
} else if (!d.structuredData?.hasJsonLd && !d.structuredData?.hasMicrodata) {
html += "<p style=\"color:#f59e0b;font-size:12px;margin-top:10px\">No structured data detected. Adding JSON-LD schema (FAQ, Article, Organization, Product) can significantly improve search visibility and enable rich results.</p>";
}
html += "</div>";
// === HREFLANG ===
if (hreflangTags.length > 0) {
html += "<div class=\"section\"><h2><span class=\"icon\">12</span> Hreflang Tags</h2>" +
"<table><tr><th>Language-Region</th></tr>" + hreflangRows + "</table></div>";
}
// === GEO ANALYSIS ===
html += "<div class=\"section\"><h2><span class=\"icon\">13</span> GEO (Generative Engine Optimization)</h2>" +
"<div class=\"stat-grid\"><div class=\"stat-item\"><div class=\"stat-value\" style=\"color:" + geoColor + "\">" + geoScore + "/100</div><div class=\"stat-label\">GEO Readiness Score</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.structuredData?.hasJsonLd ? "Yes" : "No") + "</div><div class=\"stat-label\">Schema Markup</div></div></div>" +
geoBar("Factual Content Structure", geoFactualScore(d)) +
geoBar("Entity Schema", geoEntityScore(d)) +
geoBar("Content Depth", geoContentDepthScore(d)) +
geoBar("Citeability", geoCiteabilityScore(d)) +
"<h3 style=\"font-size:14px;margin:16px 0 10px;color:#94a3b8\">What is GEO?</h3>" +
"<p style=\"font-size:13px;color:#94a3b8;margin-bottom:12px\">GEO (Generative Engine Optimization) is the practice of optimizing content for AI-powered search engines like ChatGPT, Claude, Perplexity, and Google AI Overviews. Unlike traditional SEO which targets algorithmic rankings, GEO focuses on making your content authoritative, factual, and well-structured enough for AI models to cite as sources.</p>" +
"<h3 style=\"font-size:14px;margin:16px 0 10px;color:#94a3b8\">GEO Improvement Checklist</h3>" +
"<table><tr><th>Factor</th><th>Status</th><th>Action</th></tr>" +
"<tr><td>Structured Data / Schema</td><td>" + passFail(d.structuredData?.hasJsonLd) + "</td><td>Add JSON-LD for Organization, FAQ, Article, Product schemas</td></tr>" +
"<tr><td>Heading Hierarchy</td><td>" + statusBadge(d.headingStatus) + "</td><td>One H1, logical H2-H4 hierarchy with keyword-rich headings</td></tr>" +
"<tr><td>Content Depth (1000+ words)</td><td>" + passFail((d.content?.wordCount || 0) >= 1000) + "</td><td>Expand core pages with comprehensive, authoritative content</td></tr>" +
"<tr><td>FAQ / Q&A Format</td><td>" + passFail(d.structuredData?.types?.some((s) => s.type === "FAQ") && d.structuredData?.types?.some((s) => s.found)) + "</td><td>Add FAQ schema with common questions about your topic</td></tr>" +
"<tr><td>Clear Facts & Statistics</td><td>" + passFail((d.content?.wordCount || 0) >= 500) + "</td><td>Include verifiable data points, statistics, and citations</td></tr>" +
"<tr><td>Lists & Tables</td><td>" + passFail(d.h2Count >= 3) + "</td><td>Use structured lists and comparison tables for scannability</td></tr>" +
"<tr><td>Authoritative Tone</td><td>" + passFail(d.h1Count === 1) + "</td><td>Write expert-level content with clear author attribution</td></tr>" +
"<tr><td>Meta Description CTA</td><td>" + passFail(!!d.metaDescription && d.metaDescription.length >= 120) + "</td><td>Include clear call-to-action in meta descriptions</td></tr>" +
"</table></div>";
// === ISSUES ===
html += "<div class=\"section\"><h2><span class=\"icon\">14</span> Issues & How to Fix (" + (d.issues?.length || 0) + " found)</h2>" +
"<div style=\"display:flex;gap:8px;margin-bottom:14px\">" +
"<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700\">" + badge("CRITICAL: " + criticalIssues.length, "#ef4444") + "</span>" +
"<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700\">" + badge("WARNING: " + warningIssues.length, "#f59e0b") + "</span>" +
"<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700\">" + badge("INFO: " + infoIssues.length, "#6b7280") + "</span>" +
"</div>" +
"<table><tr><th>Severity</th><th>Category</th><th>Issue</th><th>How to Fix</th></tr>" +
(allIssueRows || "<tr><td colspan=\"4\" style=\"text-align:center;color:#22c55e;padding:20px\">No issues detected. Great job!</td></tr>") +
"</table></div>";
// === ACTION PLAN ===
html += "<div class=\"section\"><h2><span class=\"icon\">15</span> Prioritized Action Plan</h2>" +
"<p style=\"color:#94a3b8;font-size:12px;margin-bottom:14px\">Recommended fixes ordered by impact and priority.</p>" +
actionPlan + "</div>";
// === FAQ RECOMMENDATIONS ===
html += "<div class=\"section\"><h2><span class=\"icon\">16</span> Recommended FAQ Schema Questions</h2>" +
"<p style=\"color:#94a3b8;font-size:12px;margin-bottom:14px\">Based on your content analysis, consider adding these FAQ items to improve featured snippet eligibility and GEO performance.</p>" +
generateFaqRecommendations(d) + "</div>";
// === GENERAL RECOMMENDATIONS ===
html += "<div class=\"section\"><h2><span class=\"icon\">17</span> SEO/GEO Best Practices Checklist</h2><ol style=\"padding-left:20px\">" +
"<li><strong>Title Tags:</strong> Keep under 60 characters. Place primary keyword near the beginning. Make each page title unique.</li>" +
"<li><strong>Meta Descriptions:</strong> Write 150-160 characters with a clear call-to-action. Include target keywords naturally.</li>" +
"<li><strong>Heading Structure:</strong> Use exactly one H1 per page. Create a logical hierarchy. Include keywords in headings.</li>" +
"<li><strong>Content Quality:</strong> Aim for 1000+ words on core pages. Write for users first, search engines second. Update content regularly.</li>" +
"<li><strong>Image Optimization:</strong> Add descriptive alt text to every image. Use WebP/AVIF format. Implement lazy loading. Compress file sizes.</li>" +
"<li><strong>Page Speed:</strong> Minimize render-blocking resources. Enable compression (Brotli/gzip). Use CDN. Optimize images and fonts.</li>" +
"<li><strong>Mobile Experience:</strong> Ensure responsive design. Test tap targets. Avoid horizontal scrolling. Optimize font sizes.</li>" +
"<li><strong>Structured Data:</strong> Implement JSON-LD schema markup. Validate with Google Rich Results Test. Add FAQ, Article, or Product schema.</li>" +
"<li><strong>Internal Linking:</strong> Create logical site architecture. Use descriptive anchor text. Ensure important pages are within 3 clicks of homepage.</li>" +
"<li><strong>Technical SEO:</strong> Submit XML sitemap. Configure robots.txt properly. Fix broken links. Implement canonical tags. Monitor crawl errors.</li>" +
"<li><strong>GEO Optimization:</strong> Structure content with clear facts, lists, and tables. Use schema markup for entities. Optimize for featured snippets. Create comprehensive, authoritative content that AI models can cite as sources.</li>" +
"<li><strong>Security:</strong> Enforce HTTPS. Set HSTS headers. Configure X-Frame-Options. Implement Content-Security-Policy.</li>" +
"</ol></div>";
// === FOOTER ===
html += "<div class=\"footer\"><p>PromptArch Vibe Architect | " + now + "</p>" +
"<p><a href=\"https://rommark.dev/tools/promptarch/\">rommark.dev/tools/promptarch</a></p></div>";
html += "</div></body></html>";
return html;
}
// --- Helper functions ---
function scoreCard(label: string, value: number): string {
return "<div class=\"score-card\"><div class=\"score-value\" style=\"color:" + sc(value) + "\">" + value + "</div><div class=\"score-label\">" + label + "</div></div>";
}
function geoBar(label: string, value: number): string {
return "<div style=\"margin:8px 0\"><div style=\"display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px\"><span>" + label + "</span><span style=\"font-weight:700;color:" + sc(value) + "\">" + value + "%</span></div>" +
"<div class=\"geo-bar\"><div class=\"geo-fill\" style=\"width:" + value + "%;background:" + sc(value) + "\"></div></div></div>";
}
function calculateGeoScore(d: SeoAuditData): number {
let score = 0;
if (d.structuredData?.hasJsonLd) score += 20;
if (d.structuredData?.hasMicrodata) score += 5;
if (d.structuredData?.types?.some((s) => s.type === "FAQ" && s.found)) score += 15;
if (d.structuredData?.types?.some((s) => s.type === "Organization" && s.found)) score += 10;
if (d.structuredData?.types?.some((s) => s.type === "Article" && s.found)) score += 5;
if (d.h1Count === 1) score += 10;
if (d.h2Count >= 3) score += 5;
if ((d.content?.wordCount || 0) >= 1000) score += 15;
else if ((d.content?.wordCount || 0) >= 500) score += 8;
if (d.openGraph?.title && d.openGraph?.description) score += 5;
if (d.metaDescription && d.metaDescription.length >= 120) score += 5;
if (d.accessibility?.hasLangAttr) score += 5;
return Math.min(score, 100);
}
function geoFactualScore(d: SeoAuditData): number {
let s = 0;
if ((d.content?.wordCount || 0) >= 1000) s += 40;
else if ((d.content?.wordCount || 0) >= 500) s += 25;
else if ((d.content?.wordCount || 0) >= 200) s += 10;
if (d.h2Count >= 3) s += 30;
if ((d.content?.paragraphCount || 0) >= 5) s += 30;
return Math.min(s, 100);
}
function geoEntityScore(d: SeoAuditData): number {
if (d.structuredData?.hasJsonLd) return 80;
if (d.structuredData?.hasMicrodata) return 50;
return 10;
}
function geoContentDepthScore(d: SeoAuditData): number {
let s = 0;
const wc = d.content?.wordCount || 0;
if (wc >= 2000) s += 40;
else if (wc >= 1000) s += 30;
else if (wc >= 500) s += 15;
if (d.h3Count >= 3) s += 20;
if (d.h4Count >= 2) s += 10;
if ((d.content?.paragraphCount || 0) >= 8) s += 15;
if ((d.links?.external || 0) >= 3) s += 15;
return Math.min(s, 100);
}
function geoCiteabilityScore(d: SeoAuditData): number {
let s = 0;
if (d.title && d.title.length >= 30) s += 15;
if (d.metaDescription && d.metaDescription.length >= 120) s += 15;
if (d.openGraph?.title) s += 10;
if (d.structuredData?.types?.some((t) => t.type === "Article" && t.found)) s += 20;
if (d.accessibility?.hasLangAttr) s += 10;
if (d.h1Count === 1) s += 10;
if (d.canonical) s += 10;
if ((d.content?.wordCount || 0) >= 1000) s += 10;
return Math.min(s, 100);
}
function generateActionPlan(d: SeoAuditData): string {
const items: { priority: string; action: string; impact: string }[] = [];
// Critical issues first
(d.issues || []).filter(i => i.severity === "critical").forEach(issue => {
items.push({ priority: "high", action: issue.message, impact: issue.category });
});
// Data-driven actions
if (!d.title) items.push({ priority: "high", action: "Add a unique title tag (50-60 chars) with primary keyword", impact: "Meta / SEO" });
if (!d.metaDescription) items.push({ priority: "high", action: "Add meta description (150-160 chars) with target keyword and CTA", impact: "Meta / CTR" });
if (!d.canonical) items.push({ priority: "high", action: "Add self-referencing canonical tag to prevent duplicate content issues", impact: "Technical" });
if (!d.viewport) items.push({ priority: "high", action: "Add viewport meta tag for proper mobile rendering", impact: "Mobile" });
if (d.protocol !== "HTTPS") items.push({ priority: "high", action: "Migrate to HTTPS with valid SSL certificate and proper redirects", impact: "Security" });
if (d.h1Count === 0) items.push({ priority: "high", action: "Add a single H1 heading with the primary keyword", impact: "Content" });
if (d.h1Count > 1) items.push({ priority: "medium", action: "Consolidate to exactly one H1 heading per page", impact: "Content" });
if (!d.structuredData?.hasJsonLd) items.push({ priority: "medium", action: "Implement JSON-LD structured data (FAQ, Organization, Article schemas)", impact: "Structured Data / GEO" });
if (!d.openGraph?.title) items.push({ priority: "medium", action: "Add Open Graph tags (og:title, og:description, og:image) for social sharing", impact: "Social" });
if (!d.twitterCard?.card) items.push({ priority: "medium", action: "Add Twitter Card meta tags for better Twitter previews", impact: "Social" });
if (!d.accessibility?.hasLangAttr) items.push({ priority: "medium", action: 'Add lang attribute to <html> tag (e.g. lang="en")', impact: "Accessibility" });
if ((d.images?.altCoverage || 0) < 90) items.push({ priority: "medium", action: "Add descriptive alt text to all images (currently " + (d.images?.altCoverage || 0) + "% coverage)", impact: "Accessibility / SEO" });
if (!d.performance?.hasPreconnect) items.push({ priority: "medium", action: "Add preconnect hints for third-party domains to improve load time", impact: "Performance" });
if (!d.performance?.usesAsyncScripts && !d.performance?.usesDeferScripts) items.push({ priority: "medium", action: "Use async or defer attributes on non-critical scripts", impact: "Performance" });
if ((d.images?.lazyLoaded || 0) === 0 && (d.images?.total || 0) > 0) items.push({ priority: "medium", action: "Implement lazy loading for images to improve initial page load", impact: "Performance" });
if (!d.structuredData?.types?.some((t) => t.type === "FAQ" && t.found)) items.push({ priority: "low", action: "Add FAQ schema with common questions about your topic for featured snippet eligibility", impact: "GEO" });
if ((d.content?.wordCount || 0) < 1000) items.push({ priority: "low", action: "Expand content to 1000+ words on core pages for better depth and authority", impact: "Content / GEO" });
if ((d.images?.total || 0) > 0 && !d.images?.lazyLoaded) items.push({ priority: "low", action: "Add loading=\"lazy\" to below-the-fold images", impact: "Performance" });
// Warning issues
(d.issues || []).filter(i => i.severity === "warning").forEach(issue => {
if (!items.some(it => it.action === issue.message)) {
items.push({ priority: "medium", action: issue.message, impact: issue.category });
}
});
// Deduplicate and limit
const seen = new Set<string>();
const unique = items.filter(item => {
if (seen.has(item.action)) return false;
seen.add(item.action);
return true;
}).slice(0, 20);
return unique.map(item =>
"<div class=\"action-item\"><div><span class=\"action-priority " + item.priority + "\">" + item.priority + "</span></div>" +
"<div><div style=\"font-weight:600;font-size:13px\">" + item.action + "</div>" +
"<div style=\"font-size:11px;color:#94a3b8;margin-top:2px\">Impact: " + item.impact + "</div></div></div>"
).join("");
}
function generateFaqRecommendations(d: SeoAuditData): string {
const domain = d.domain || "this site";
const title = d.title || domain;
const questions: string[] = [];
questions.push("What is " + title + "?<br>Provide a clear, concise answer (40-60 words) explaining what " + domain + " offers and its primary value proposition.");
questions.push("How does " + domain + " work?<br>Explain the core functionality, process, or service in simple terms.");
questions.push("What are the main benefits of using " + domain + "?<br>List 3-5 key benefits with brief explanations.");
questions.push("Is " + domain + " free or paid?<br>Provide clear pricing information or state if the service is free.");
questions.push("How do I get started with " + domain + "?<br>Outline the steps a new user should take to begin.");
if (d.protocol === "HTTPS") {
questions.push("Is " + domain + " secure?<br>Confirm security measures in place (SSL, data protection, etc.).");
}
if ((d.content?.wordCount || 0) >= 300) {
questions.push("What makes " + domain + " different from competitors?<br>Highlight unique selling points and differentiators.");
}
return questions.map((q, i) => {
const parts = q.split("<br>");
return "<div style=\"padding:12px 0;border-bottom:1px solid #334155\"><div style=\"font-weight:700;font-size:13px;color:#e2e8f0\">" + (i + 1) + ". " + parts[0] + "</div>" +
"<div style=\"font-size:12px;color:#94a3b8;margin-top:4px\">" + parts[1] + "</div></div>";
}).join("");
}
export function downloadSeoReport(d: SeoAuditData, format: "html" | "pdf") {
const html = generateSeoReportHtml(d);
if (format === "html") {
const blob = new Blob([html], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "seo-geo-audit-" + (d.domain || "report") + "-" + new Date().toISOString().slice(0, 10) + ".html";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} else {
const iframe = document.createElement("iframe");
iframe.style.position = "fixed";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.width = "0";
iframe.style.height = "0";
iframe.style.border = "none";
document.body.appendChild(iframe);
const doc = iframe.contentDocument || iframe.contentWindow?.document;
if (doc) {
doc.open();
doc.write(html);
doc.close();
iframe.onload = () => {
setTimeout(() => {
iframe.contentWindow?.focus();
iframe.contentWindow?.print();
setTimeout(() => document.body.removeChild(iframe), 5000);
}, 600);
};
}
}
}

View File

@@ -852,7 +852,13 @@ When the user asks to "Modify", "Change", or "Adjust" something:
AGENTS & CAPABILITIES: AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text. - content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:** - seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
**CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html> - Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background - DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit) - Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
@@ -865,6 +871,7 @@ AGENTS & CAPABILITIES:
- Add animated pulse effects on key metrics - Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto - Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load - Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars. - smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans. - pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets. - code: Software Architect. Provide logic, algorithms, and backend snippets.

View File

@@ -509,13 +509,72 @@ When the user asks to "Modify", "Change", or "Adjust" something:
AGENTS & CAPABILITIES: AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text. - content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>. DARK THEME. Tailwind CDN. Large animated SVG progress rings. Color-coded scoring. Google-style dashboard aesthetics. - seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
**CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
- Section cards with subtle borders (border-white/10) and backdrop-blur
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
- Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars. - smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans. - pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets. - code: Software Architect. Provide logic, algorithms, and backend snippets.
- design: UI/UX Designer. Create high-fidelity mockups and components. - design: UI/UX Designer. Create high-fidelity mockups and components.
- web: Frontend Developer. Build responsive sites. Use [PREVIEW:web:html]. Tailwind CSS CDN by default. - web: Frontend Developer. Build responsive sites. Use [PREVIEW:web:html]. Tailwind CSS CDN by default.
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. Use [PREVIEW:app:javascript]. - app: Mobile App Developer. Create mobile-first interfaces and dashboards. Use [PREVIEW:app:javascript].
- leads: Leads Finder. You are a SILENT data tool. DO NOT chat, greet, ask questions, or explain. Your ONLY job: use [WEB_SEARCH:query] to find leads, then output them as an HTML table. NEVER say "I'd be happy to help" or "Let me search" or "Here are some leads" — just output data.
**OUTPUT SEQUENCE (follow exactly):**
1. Use [WEB_SEARCH:query] multiple times with different queries to find leads
2. One line summary: "Found X leads in Y niche across Z platforms"
3. [PREVIEW:leads:html] with the COMPLETE HTML below — replace placeholders with real data
4. [/PREVIEW]
5. Brief changelog bullet points
That is ALL. No greetings, no filler, no conversation.
**HTML TEMPLATE — copy this exactly, fill in the data:**
<!DOCTYPE html><html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0f0f;color:#e2e8f0;padding:24px}
.header{text-align:center;margin-bottom:24px}.header h1{font-size:24px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.header p{color:#94a3b8;font-size:13px;margin-top:4px}
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}.stat{background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);border-radius:10px;padding:10px 16px;text-align:center;flex:1;min-width:100px}.stat .num{font-size:20px;font-weight:800;color:#10b981}.stat .lbl{font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-top:2px}
table{width:100%;border-collapse:collapse;background:rgba(15,23,42,0.6);border-radius:12px;overflow:hidden;border:1px solid rgba(148,163,184,0.1)}
thead th{background:rgba(16,185,129,0.12);color:#10b981;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;padding:12px 14px;text-align:left;border-bottom:1px solid rgba(148,163,184,0.1)}
tbody tr{border-bottom:1px solid rgba(148,163,184,0.06)}tbody tr:hover{background:rgba(16,185,129,0.04)}
tbody td{padding:10px 14px;font-size:13px;color:#cbd5e1}
.name{font-weight:700;color:#f1f5f9}.url{color:#10b981;text-decoration:none;font-size:12px}.url:hover{text-decoration:underline}
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600}
.badge-instagram{background:rgba(225,48,108,0.15);color:#f472b6}.badge-twitter{background:rgba(59,130,246,0.15);color:#60a5fa}.badge-linkedin{background:rgba(59,130,246,0.15);color:#93c5fd}.badge-youtube{background:rgba(239,68,68,0.15);color:#fca5a5}.badge-tiktok{background:rgba(168,85,247,0.15);color:#c084fc}
.followers{color:#10b981;font-weight:700}.region{color:#94a3b8;font-size:12px}.bio{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
</style></head><body>
<div class="header"><h1>Leads Report</h1><p>Generated by PromptArch</p></div>
<div class="stats">
<div class="stat"><div class="num">TOTAL_COUNT</div><div class="lbl">Total Leads</div></div>
<div class="stat"><div class="num">COMBINED_FOLLOWERS</div><div class="lbl">Combined Reach</div></div>
<div class="stat"><div class="num">TOP_PLATFORM</div><div class="lbl">Top Platform</div></div>
<div class="stat"><div class="num">TOP_REGION</div><div class="lbl">Top Region</div></div>
</div>
<table><thead><tr><th>#</th><th>Name</th><th>Platform</th><th>Followers</th><th>Region</th><th>Bio</th><th>Link</th></tr></thead><tbody>
<tr><td>1</td><td class="name">NAME</td><td><span class="badge badge-PLATFORM">PLATFORM</span></td><td class="followers">FOLLOWERS</td><td class="region">REGION</td><td class="bio" title="FULL BIO">SHORT BIO</td><td><a class="url" href="URL" target="_blank">Visit &rarr;</a></td></tr>
<!-- Repeat <tr> for each lead. Use badge-instagram/badge-twitter/badge-linkedin/badge-youtube/badge-tiktok -->
</tbody></table></body></html>
**RULES:**
- Find 20+ leads. Sort by relevance/follower count.
- Use exact follower counts (e.g. "44.1K", "1.2M").
- REAL leads from search results only — never fabricate profiles or URLs.
- Fill STATS div with real counts from your results.
- The entire HTML must be between [PREVIEW:leads:html] and [/PREVIEW].
CANVAS MODE: CANVAS MODE:
- When building, designing, or auditing, you MUST use the [PREVIEW] tag. - When building, designing, or auditing, you MUST use the [PREVIEW] tag.

View File

@@ -1134,7 +1134,13 @@ When the user asks to "Modify", "Change", or "Adjust" something:
AGENTS & CAPABILITIES: AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text. - content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:** - seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
**CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html> - Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background - DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit) - Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
@@ -1147,6 +1153,7 @@ AGENTS & CAPABILITIES:
- Add animated pulse effects on key metrics - Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto - Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load - Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars. - smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans. - pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets. - code: Software Architect. Provide logic, algorithms, and backend snippets.

View File

@@ -865,7 +865,13 @@ When the user asks to "Modify", "Change", or "Adjust" something:
AGENTS & CAPABILITIES: AGENTS & CAPABILITIES:
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text. - content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
- seo: SEO Specialist. Create stunning SEO audit reports. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.** **CRITICAL DESIGN REQUIREMENTS:** - seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
**CRITICAL DESIGN REQUIREMENTS:**
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html> - Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background - DARK THEME: bg-slate-900 or bg-gray-900 as primary background
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit) - Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
@@ -878,6 +884,7 @@ AGENTS & CAPABILITIES:
- Add animated pulse effects on key metrics - Add animated pulse effects on key metrics
- Full-width responsive layout, max-w-4xl mx-auto - Full-width responsive layout, max-w-4xl mx-auto
- Include inline <script> for animating the progress rings on load - Include inline <script> for animating the progress rings on load
- smm: Social Media Manager. Create multi-platform content plans and calendars. - smm: Social Media Manager. Create multi-platform content plans and calendars.
- pm: Project Manager. Create PRDs, timelines, and action plans. - pm: Project Manager. Create PRDs, timelines, and action plans.
- code: Software Architect. Provide logic, algorithms, and backend snippets. - code: Software Architect. Provide logic, algorithms, and backend snippets.

View File

@@ -1,6 +1,6 @@
{ {
"name": "promptarch", "name": "promptarch",
"version": "1.4.0", "version": "1.9.0",
"description": "Transform vague ideas into production-ready prompts and PRDs", "description": "Transform vague ideas into production-ready prompts and PRDs",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",