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

@@ -1,144 +1,324 @@
# Changelog # Changelog
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
## [1.4.0] - 2026-03-18 19:57 UTC
### Added
### 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
- **Review Code Button** — Post-coding action to send generated code back to AI for review - **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
- `reviewCode()` function sends code with review-focused prompt - **Auto Canvas** - Leads Finder is now a visual agent that auto-opens the canvas when preview data is available
- Emerald-green "Review" button alongside Preview and Modify
- 3-column post-coding action grid: Preview / Review / Modify ### Fixed
- **Web Search Grounding** — Enrich AI prompts with live web search results - **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
- `lib/services/search-api.ts` — SearXNG public API wrapper with 4 instance fallback
- `/api/search` route — server-side search proxy endpoint ### Technical Details
- Toggle button in toolbar to enable/disable (amber highlight when active) - Files modified: 2 (AIAssist.tsx, openrouter.ts)
- Shows "Searching the web..." status while fetching
- Appends top 5 results as `[WEB SEARCH CONTEXT]` to user prompt ## [2.2.0] - 2026-03-19 04:44 UTC
- **Responsive Preview** — Device size selector in canvas panel
- Full / Desktop (1280px) / Tablet (768px) / Mobile (375px) buttons ### Added
- Centered device frame with border, shadow, and scroll on overflow - **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)
- Only visible in preview mode, below Live Render / Inspect Code tabs - **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
### Fixed
- **Model selector text color** — Option text now white on dark theme (`bg-[#0b1414] text-white`) ### Technical Details
- **Button text overflow** — Shortened labels (Preview/Review/Modify), added `min-w-0` for proper truncation - Files modified: 3 (AIAssist.tsx, translations.ts, openrouter.ts)
- **Duplicate activateArtifact button** — Original button now hides when post-coding action row is shown
### Technical Details ## [2.1.0] - 2026-03-18 22:06 UTC
- Files modified: 1 (AIAssist.tsx: +85/-11 lines)
- Files added: 2 (`lib/services/search-api.ts`, `app/api/search/route.ts`) ### Added
- New state: `deviceSize`, `webSearchEnabled` - **Comprehensive 17-Section Report** — Full rebuild of SEO/GEO audit report generator with all industry-standard sections:
- New function: `reviewCode()` - Executive Summary with stats grid
- 6-Category Scoring Breakdown with progress bars (Overall, Technical, Content, Performance, Social, GEO)
## [1.3.0] - 2026-03-18 18:51 UTC - 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)
### Added - Heading Structure with hierarchy visualization
- **OpenRouter Integration** — 4th AI provider with API key auth, 20+ model support - Links Analysis with sample external links table
- New `lib/services/openrouter.ts` streaming service - Images & Alt Text with missing-alt report
- Provider selector in AI Assist: Qwen, Ollama, Z.AI, OpenRouter - Content Analysis with readability recommendations
- Default model: `google/gemini-2.0-flash-exp:free` - Performance Signals (12 metrics including encoding, scripts, preconnect, preload, DNS prefetch, async/defer)
- Custom model selector with popular free/paid model presets - Accessibility (4 checks: lang, ARIA, first-image alt, alt coverage)
- Settings panel: API key input with validation and model picker - Structured Data / Schema detection
- **Plan-First Workflow** — AI now generates a structured plan before code - Hreflang Tags
- PLAN MODE instructions injected into all 4 service system prompts - GEO Analysis with readiness score, 4 sub-scores (Factual, Entity, Content Depth, Citeability), and improvement checklist
- Plan card UI with architecture, tech stack, files, and steps - Issues & How to Fix (severity badges, category-specific fix instructions)
- `parsePlanFromResponse()` extracts plans from AI markdown output - Prioritized Action Plan (high/medium/low with impact labels)
- `[PLAN]` tags hidden from displayed chat messages - Recommended FAQ Schema Questions (auto-generated based on domain)
- Three action buttons: **Modify Plan** / **Start Coding** / **Skip to Code** - SEO/GEO Best Practices Checklist (12 items)
- **Post-Coding UX** — Preview + Request Modifications after code generation - **GEO Scoring Engine** — 0-100 GEO readiness score based on schema markup, content depth, heading hierarchy, and citeability signals
- After "Start Coding" approval, AI generates code with `[PREVIEW]` tags - **Auto-Generated Action Plan** — 20+ prioritized fixes derived from audit data with severity and impact labels
- Canvas opens automatically with renderable previews - **Print-Optimized CSS** — Clean white-on-dark print styles for professional PDF output
- Two post-coding buttons: **Preview** (re-opens canvas) and **Request Modifications**
- `isApproval` flag prevents stale React closure bugs in approval flow ### Technical Details
- **Enhanced Prompt Engine** — New modular prompt enhancement system - Files modified: 1 (seo-report.ts) — complete rewrite, 530+ lines
- `lib/enhance-engine.ts` with 9 enhancement strategies
- Strategies: clarify, add-context, add-constraints, structure, add-examples, set-tone, expand, simplify, chain-of-thought
- Context-aware enhancement based on detected intent type ## [2.0.1] - 2026-03-18 21:56 UTC
- 11+ intent detection patterns (coding, creative, analysis, etc.)
- Smart strategy selection per intent for optimal prompt refinement ### Fixed
- **Streaming Plan Mode** — Real-time plan parsing during AI response - **PDF Export** — Replaced `window.open` with hidden iframe approach for reliable print dialog trigger (no popup blocker issues)
- `wasIdle` flag captures initial request phase before state updates - **PDF Export** — Initial fix using Blob URL (superseded by iframe approach)
- Canvas display suppressed during plan generation, enabled after approval
- Post-stream routing: plan card for initial requests, preview for approvals ### Added
- Tab `showCanvas` state gated by plan phase - **Inline SEO Export Buttons** — Export HTML/PDF buttons now appear directly in chat messages after SEO audit, not just in post-coding action bar
### Changed ### Technical Details
- **AIAssist.tsx** — Major refactor for plan-first flow - Files modified: 2 (AIAssist.tsx, seo-report.ts)
- `handleSendMessage` now accepts `isApproval` parameter to prevent stale closures
- `approveAndGenerate()` passes `isApproval=true` to bypass idle detection
- `assistStep` state machine: `idle -> plan -> generating -> preview` ## [2.0.0] - 2026-03-18 21:33 UTC
- `parseStreamingContent()` filters `[PLAN]` tags from displayed output
- **PromptEnhancer.tsx** — Rebuilt with modular enhance engine ### Added
- Moved enhancement logic to `lib/enhance-engine.ts` - **SEO Audit Export** — Export SEO/GEO audit reports as HTML or PDF with comprehensive fix instructions
- Added expand, simplify, and chain-of-thought strategies - **SEO Report Generator** — Standalone `lib/seo-report.ts` utility with color-coded scores, issue tables with fix instructions, and print-friendly CSS
- Improved intent detection and strategy mapping - **Default Vibe Architect** — Welcome screen now opens to Vibe Architect by default (was Prompt Enhancer)
- **SettingsPanel.tsx** — Added OpenRouter provider configuration - **Vibe Architect Dedicated Route** — Full-screen immersive mode at `/vibe` with `vibeMode` prop
- API key input with validation
- Model selector with preset dropdown ### Changed
- Provider-specific endpoint display - **General Agent Plain Chat** — General mode no longer triggers plan/code flow, now works as plain chat
- **model-adapter.ts** — Extended with OpenRouter provider support - **SEO Follow-up Fix** — Non-visual agents (SEO, content, SMM) preserve existing canvas on follow-up messages
- New adapter mapping for OpenRouter service
- Unified interface across all 4 providers ### Technical Details
- **translations.ts** — Added i18n keys for plan mode, OpenRouter, post-coding actions - Files modified: 3 (AIAssist.tsx, page.tsx)
- Keys: `modifyPlan`, `startCoding`, `skipToCode`, `requestModifications` - Files added: 2 (seo-report.ts, vibe/page.tsx)
- OpenRouter provider labels and descriptions
- English, Russian, Hebrew translations updated (https://semver.org/spec/v2.0.0.html).
- **store.ts** — Added `selectedProvider` state for multi-provider selection
- **types/index.ts** — Added `PreviewData` interface for typed canvas rendering ## [1.9.0] - 2026-03-18 21:05 UTC
- **adapter-instance.ts** — Registered OpenRouter in provider registry
### Changed
### Fixed - **Global Rebrand: AI Assist -> Vibe Architect** — All instances renamed across UI, sidebar, translations (EN/RU/HE)
- **Stale React closure** in `approveAndGenerate``setAssistStep("generating")` followed by `handleSendMessage()` read stale `assistStep` value. Fixed with explicit `isApproval` boolean parameter. - **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
- **Plan card reappearing after code generation** — Post-stream logic now correctly routes to `preview` mode after approval coding, not back to `plan` mode.
- **Canvas auto-opening during plan phase** — `setShowCanvas(true)` in `onChunk` now gated by `!wasIdle` flag. ### Technical Details
- **i18n missing keys** — Added `syncComplete` for Hebrew, fixed double commas in multiple translation strings. - Files modified: 3 (Sidebar.tsx, AIAssist.tsx, translations.ts)
### Technical Details ## [1.8.0] - 2026-03-18 21:02 UTC
- Files modified: 11 (960 insertions, 194 deletions)
- Files added: 2 (`lib/enhance-engine.ts`, `lib/services/openrouter.ts`) ### Added
- Total project lines: ~10,179 across core files - **Vibe Architect Dedicated Mode** — Full-screen immersive AI coding experience
- System prompt PLAN MODE block added to: `qwen-oauth.ts`, `ollama-cloud.ts`, `zai-plan.ts`, `openrouter.ts` - New route: `/vibe` ([rommark.dev/tools/promptarch/vibe/](https://rommark.dev/tools/promptarch/vibe/))
- No sidebar, no navigation — just AI Assist in full screen
## [1.2.0] - 2026-01-19 19:16 UTC - Rebranded as "Vibe Architect" with dedicated messaging
- `vibeMode` prop on AIAssist component for label overrides
### Added
- **SEO Agent Behavior Fixes** ### Fixed
- SEO agent now stays locked and answers queries through an SEO lens - **SEO follow-up preview** — Canvas no longer breaks when asking follow-up questions after SEO audit
- Smart agent suggestions via `[SUGGEST_AGENT:xxx]` marker for clearly non-SEO tasks - `isModifying` overlay only triggers for visual agents (code, web, app, design) — not SEO/content/SMM
- Visual suggestion banner with Switch/Dismiss buttons - Non-visual agent follow-ups preserve existing canvas if no new preview is generated
- Prevented unwanted agent auto-switching mid-response
- **z.ai API Validation** ### Technical Details
- Real-time API key validation with 500ms debounce - Files added: 1 (`app/vibe/page.tsx`)
- Inline status indicators: - Files modified: 1 (AIAssist.tsx: +8/-3 lines)
- Checkmark with "Validated Xm ago" for valid keys - New prop: `vibeMode` on AIAssist component
- Red X with error message for invalid keys
- Loading spinner during validation ## [1.7.0] - 2026-03-18 20:44 UTC
- "Test Connection" button for manual re-validation
- Persistent validation cache (5 minutes) in localStorage ### Added
- **Comprehensive SEO Audit Engine** — Industry-grade URL analysis modeled after Screaming Frog, Ahrefs, Sitebulb, and Semrush
### Changed - Full meta tag analysis: title (length/status), description (length/status), keywords, viewport, charset, robots directives
- Updated Settings panel with improved UX for API key management - Open Graph & Twitter Card validation
- Enhanced agent selection behavior to prevent unintended switches - Heading structure audit: H1 count check, hierarchy analysis (H1-H6), multiple H1 detection
- Link analysis: internal/external/nofollow counts, external link sampling
## [1.1.0] - 2025-12-29 17:55 UTC - Image audit: total count, alt text coverage %, lazy loading detection, missing alt samples
- Content analysis: word count, sentence count, paragraph count, avg words/sentence
### Added - Structured data detection: JSON-LD, Microdata, 12+ schema types (Article, FAQ, Product, LocalBusiness, etc.)
- GitHub integration for pushing AI-generated artifacts - Performance signals: server info, response time, HTML size, external scripts/stylesheets, inline styles, preconnect/preload/dns-prefetch, async/defer detection
- XLSX export functionality for Google Ads campaigns - Accessibility checks: lang attribute, ARIA labels, first image alt text
- High-fidelity HTML report generation - Hreflang tag detection for international SEO
- OAuth token management for Qwen API - Canonical tag validation with mismatch detection
- **Automated scoring**: Overall, Technical, Content, Performance, Social scores (0-100)
## [1.0.0] - 2025-12-29 13:51 UTC - **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
### Added - No more "Start Coding" or "Modify Plan" buttons for non-code agent responses
- Initial release of PromptArch - SEO agent goes straight to preview mode after generating audit reports
- Multi-provider AI support (Qwen, Ollama, Z.AI) - Plan card only shown for code/general agents
- Prompt Enhancer with 11+ intent patterns
- PRD Generator with structured output ### Changed
- Action Plan generator with framework recommendations - `/api/fetch-url` route completely rewritten: 300+ lines of comprehensive HTML parsing and scoring
- Visual canvas for live code rendering - SEO agent data injection now passes structured audit data with scores, issues, and category breakdowns
- Multi-language support (English, Russian, Hebrew)
### 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
### Added
- **Review Code Button** — Post-coding action to send generated code back to AI for review
- `reviewCode()` function sends code with review-focused prompt
- Emerald-green "Review" button alongside Preview and Modify
- 3-column post-coding action grid: Preview / Review / Modify
- **Web Search Grounding** — Enrich AI prompts with live web search results
- `lib/services/search-api.ts` — SearXNG public API wrapper with 4 instance fallback
- `/api/search` route — server-side search proxy endpoint
- Toggle button in toolbar to enable/disable (amber highlight when active)
- Shows "Searching the web..." status while fetching
- Appends top 5 results as `[WEB SEARCH CONTEXT]` to user prompt
- **Responsive Preview** — Device size selector in canvas panel
- Full / Desktop (1280px) / Tablet (768px) / Mobile (375px) buttons
- Centered device frame with border, shadow, and scroll on overflow
- Only visible in preview mode, below Live Render / Inspect Code tabs
### Fixed
- **Model selector text color** — Option text now white on dark theme (`bg-[#0b1414] text-white`)
- **Button text overflow** — Shortened labels (Preview/Review/Modify), added `min-w-0` for proper truncation
- **Duplicate activateArtifact button** — Original button now hides when post-coding action row is shown
### Technical Details
- Files modified: 1 (AIAssist.tsx: +85/-11 lines)
- Files added: 2 (`lib/services/search-api.ts`, `app/api/search/route.ts`)
- New state: `deviceSize`, `webSearchEnabled`
- New function: `reviewCode()`
## [1.3.0] - 2026-03-18 18:51 UTC
### Added
- **OpenRouter Integration** — 4th AI provider with API key auth, 20+ model support
- New `lib/services/openrouter.ts` streaming service
- Provider selector in AI Assist: Qwen, Ollama, Z.AI, OpenRouter
- Default model: `google/gemini-2.0-flash-exp:free`
- Custom model selector with popular free/paid model presets
- Settings panel: API key input with validation and model picker
- **Plan-First Workflow** — AI now generates a structured plan before code
- PLAN MODE instructions injected into all 4 service system prompts
- Plan card UI with architecture, tech stack, files, and steps
- `parsePlanFromResponse()` extracts plans from AI markdown output
- `[PLAN]` tags hidden from displayed chat messages
- Three action buttons: **Modify Plan** / **Start Coding** / **Skip to Code**
- **Post-Coding UX** — Preview + Request Modifications after code generation
- After "Start Coding" approval, AI generates code with `[PREVIEW]` tags
- Canvas opens automatically with renderable previews
- Two post-coding buttons: **Preview** (re-opens canvas) and **Request Modifications**
- `isApproval` flag prevents stale React closure bugs in approval flow
- **Enhanced Prompt Engine** — New modular prompt enhancement system
- `lib/enhance-engine.ts` with 9 enhancement strategies
- Strategies: clarify, add-context, add-constraints, structure, add-examples, set-tone, expand, simplify, chain-of-thought
- Context-aware enhancement based on detected intent type
- 11+ intent detection patterns (coding, creative, analysis, etc.)
- Smart strategy selection per intent for optimal prompt refinement
- **Streaming Plan Mode** — Real-time plan parsing during AI response
- `wasIdle` flag captures initial request phase before state updates
- Canvas display suppressed during plan generation, enabled after approval
- Post-stream routing: plan card for initial requests, preview for approvals
- Tab `showCanvas` state gated by plan phase
### Changed
- **AIAssist.tsx** — Major refactor for plan-first flow
- `handleSendMessage` now accepts `isApproval` parameter to prevent stale closures
- `approveAndGenerate()` passes `isApproval=true` to bypass idle detection
- `assistStep` state machine: `idle -> plan -> generating -> preview`
- `parseStreamingContent()` filters `[PLAN]` tags from displayed output
- **PromptEnhancer.tsx** — Rebuilt with modular enhance engine
- Moved enhancement logic to `lib/enhance-engine.ts`
- Added expand, simplify, and chain-of-thought strategies
- Improved intent detection and strategy mapping
- **SettingsPanel.tsx** — Added OpenRouter provider configuration
- API key input with validation
- Model selector with preset dropdown
- Provider-specific endpoint display
- **model-adapter.ts** — Extended with OpenRouter provider support
- New adapter mapping for OpenRouter service
- Unified interface across all 4 providers
- **translations.ts** — Added i18n keys for plan mode, OpenRouter, post-coding actions
- Keys: `modifyPlan`, `startCoding`, `skipToCode`, `requestModifications`
- OpenRouter provider labels and descriptions
- English, Russian, Hebrew translations updated
- **store.ts** — Added `selectedProvider` state for multi-provider selection
- **types/index.ts** — Added `PreviewData` interface for typed canvas rendering
- **adapter-instance.ts** — Registered OpenRouter in provider registry
### Fixed
- **Stale React closure** in `approveAndGenerate``setAssistStep("generating")` followed by `handleSendMessage()` read stale `assistStep` value. Fixed with explicit `isApproval` boolean parameter.
- **Plan card reappearing after code generation** — Post-stream logic now correctly routes to `preview` mode after approval coding, not back to `plan` mode.
- **Canvas auto-opening during plan phase** — `setShowCanvas(true)` in `onChunk` now gated by `!wasIdle` flag.
- **i18n missing keys** — Added `syncComplete` for Hebrew, fixed double commas in multiple translation strings.
### Technical Details
- Files modified: 11 (960 insertions, 194 deletions)
- Files added: 2 (`lib/enhance-engine.ts`, `lib/services/openrouter.ts`)
- Total project lines: ~10,179 across core files
- System prompt PLAN MODE block added to: `qwen-oauth.ts`, `ollama-cloud.ts`, `zai-plan.ts`, `openrouter.ts`
## [1.2.0] - 2026-01-19 19:16 UTC
### Added
- **SEO Agent Behavior Fixes**
- SEO agent now stays locked and answers queries through an SEO lens
- Smart agent suggestions via `[SUGGEST_AGENT:xxx]` marker for clearly non-SEO tasks
- Visual suggestion banner with Switch/Dismiss buttons
- Prevented unwanted agent auto-switching mid-response
- **z.ai API Validation**
- Real-time API key validation with 500ms debounce
- Inline status indicators:
- Checkmark with "Validated Xm ago" for valid keys
- Red X with error message for invalid keys
- Loading spinner during validation
- "Test Connection" button for manual re-validation
- Persistent validation cache (5 minutes) in localStorage
### Changed
- Updated Settings panel with improved UX for API key management
- Enhanced agent selection behavior to prevent unintended switches
## [1.1.0] - 2025-12-29 17:55 UTC
### Added
- GitHub integration for pushing AI-generated artifacts
- XLSX export functionality for Google Ads campaigns
- High-fidelity HTML report generation
- OAuth token management for Qwen API
## [1.0.0] - 2025-12-29 13:51 UTC
### Added
- Initial release of PromptArch
- Multi-provider AI support (Qwen, Ollama, Z.AI)
- Prompt Enhancer with 11+ intent patterns
- PRD Generator with structured output
- Action Plan generator with framework recommendations
- Visual canvas for live code rendering
- Multi-language support (English, Russian, Hebrew)

365
README.md
View File

@@ -1,177 +1,188 @@
# PromptArch: AI Orchestration Platform # PromptArch: AI Orchestration Platform
> **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). > **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)
> **Learn more about this architecture [here](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).**
> **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning. ---
Transform vague ideas into production-ready prompts and PRDs. PromptArch is an AI orchestration platform designed for software architects and Vibe Coders, featuring a **plan-first workflow** with multi-provider AI support and live canvas rendering. > **Fork Note**: This project is a specialized fork of [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix), reimagined as a modern web-based platform for visual prompt engineering and product planning.
**Developed by Roman | RyzenAdvanced** Transform vague ideas into production-ready prompts and PRDs. PromptArch is an AI orchestration platform designed for software architects and Vibe Coders, featuring a **plan-first workflow** with multi-provider AI support and live canvas rendering.
- **Gitea Repository**: [admin/PromptArch](https://github.rommark.dev/admin/PromptArch) **Developed by Roman | RyzenAdvanced**
- **Live Site**: [rommark.dev/tools/promptarch](https://rommark.dev/tools/promptarch/)
- **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem) - **Gitea Repository**: [admin/PromptArch](https://github.rommark.dev/admin/PromptArch)
- **Live Site**: [rommark.dev/tools/promptarch](https://rommark.dev/tools/promptarch/)
## Core Capabilities - **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
| Feature | Description | ## Core Capabilities
|---------|-------------|
| **AI Assist** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview | | Feature | Description |
| **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 | | **Vibe Architect** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview |
| **Action Plan** | Decompose PRDs into actionable development steps and framework recommendations | | **Prompt Enhancer** | Refine vague prompts into surgical instructions using 9 enhancement strategies and 11+ intent patterns |
| **Google Ads Generator** | Generate ad campaigns with XLSX and HTML report export | | **PRD Generator** | Convert ideas into structured Product Requirements Documents |
| **Slides Generator** | Create presentation decks from prompts | | **Action Plan** | Decompose PRDs into actionable development steps and framework recommendations |
| **Market Researcher** | AI-powered market research and analysis | | **Google Ads Generator** | Generate ad campaigns with XLSX and HTML report export |
| **Slides Generator** | Create presentation decks from prompts |
## Features | **Market Researcher** | AI-powered market research and analysis |
### Plan-First Workflow (v1.3.0) ## Features
- AI generates a structured plan (architecture, tech stack, files, steps) before any code
- Plan Review Card with **Modify Plan**, **Start Coding**, and **Skip to Code** actions ### Plan-First Workflow (v1.3.0)
- After code generation: **Preview** canvas + **Request Modifications** buttons - AI generates a structured plan (architecture, tech stack, files, steps) before any code
- Streaming plan mode with real-time parsing and canvas suppression - Plan Review Card with **Modify Plan**, **Start Coding**, and **Skip to Code** actions
- After code generation: **Preview** canvas + **Request Modifications** buttons
### Multi-Provider AI (4 Providers) - Streaming plan mode with real-time parsing and canvas suppression
| Provider | Auth | Models |
|----------|------|--------| ### Multi-Provider AI (4 Providers)
| **Qwen Code** | OAuth (2,000 free req/day) | Qwen Coder models | | Provider | Auth | Models |
| **Ollama Cloud** | API Key | Open-source models | |----------|------|--------|
| **Z.AI Plan** | API Key | GLM general + coding models | | **Qwen Code** | OAuth (2,000 free req/day) | Qwen Coder models |
| **OpenRouter** | API Key | 20+ models (Gemini, Llama, Mistral, etc.) | | **Ollama Cloud** | API Key | Open-source models |
| **Z.AI Plan** | API Key | GLM general + coding models |
### Visual Canvas | **OpenRouter** | API Key | 20+ models (Gemini, Llama, Mistral, etc.) |
- Live code rendering with `[PREVIEW]` tags
- HTML, React, Python, and more — rendered in-browser ### Visual Canvas
- Auto-detect renderable vs. code-only previews - Live code rendering with `[PREVIEW]` tags
- Responsive preview with device size selector (Full / Desktop / Tablet / Mobile) - HTML, React, Python, and more — rendered in-browser
- Auto-detect renderable vs. code-only previews
### Code Review & Web Search - Responsive preview with device size selector (Full / Desktop / Tablet / Mobile)
- **Review Code** — Send generated code back to AI for bug/security/performance review
- **Web Search Grounding** — Toggle to enrich prompts with live web search results via SearXNG ### Code Review & Web Search
- **Review Code** — Send generated code back to AI for bug/security/performance review
### Enhanced Prompt Engine - **Web Search Grounding** — Toggle to enrich prompts with live web search results via SearXNG
- 9 strategies: clarify, add-context, add-constraints, structure, add-examples, set-tone, expand, simplify, chain-of-thought
- Context-aware strategy selection based on detected intent ### Enhanced Prompt Engine
- 11+ intent detection patterns (coding, creative, analysis, etc.) - 9 strategies: clarify, add-context, add-constraints, structure, add-examples, set-tone, expand, simplify, chain-of-thought
- Context-aware strategy selection based on detected intent
### Other - 11+ intent detection patterns (coding, creative, analysis, etc.)
- Multi-language support (English, Russian, Hebrew)
- Download generated artifacts as ZIP ### Other
- Push to GitHub integration - Multi-language support (English, Russian, Hebrew)
- Resilient multi-tier provider fallbacks - Download generated artifacts as ZIP
- Push to GitHub integration
## Quick Start - Resilient multi-tier provider fallbacks
1. **Clone & Install**: ## Quick Start
```bash
git clone https://github.rommark.dev/admin/PromptArch.git 1. **Clone & Install**:
cd PromptArch ```bash
npm install git clone https://github.rommark.dev/admin/PromptArch.git
``` cd PromptArch
npm install
2. **Configuration**: ```
Copy `.env.example` to `.env` and add your API keys:
```bash 2. **Configuration**:
cp .env.example .env Copy `.env.example` to `.env` and add your API keys:
``` ```bash
cp .env.example .env
Configure at least one provider: ```
- **Qwen**: Get OAuth credentials from [qwen.ai](https://qwen.ai)
- **Ollama**: Get API key from [ollama.com/cloud](https://ollama.com/cloud) Configure at least one provider:
- **Z.AI**: Get API key from [docs.z.ai](https://docs.z.ai) - **Qwen**: Get OAuth credentials from [qwen.ai](https://qwen.ai)
- **OpenRouter**: Get API key from [openrouter.ai/keys](https://openrouter.ai/keys) (free tier available) - **Ollama**: Get API key from [ollama.com/cloud](https://ollama.com/cloud)
- **Z.AI**: Get API key from [docs.z.ai](https://docs.z.ai)
3. **Launch**: - **OpenRouter**: Get API key from [openrouter.ai/keys](https://openrouter.ai/keys) (free tier available)
```bash
npm run dev 3. **Launch**:
``` ```bash
npm run dev
4. Open [http://localhost:3000](http://localhost:3000) to begin. ```
## Tech Stack 4. Open [http://localhost:3000](http://localhost:3000) to begin.
- **Framework**: Next.js 15 (App Router, Turbopack) ## Tech Stack
- **Styling**: Tailwind CSS
- **State Management**: Zustand - **Framework**: Next.js 15 (App Router, Turbopack)
- **Components**: shadcn/ui (Radix UI) - **Styling**: Tailwind CSS
- **Icons**: Lucide React - **State Management**: Zustand
- **Markdown**: react-markdown - **Components**: shadcn/ui (Radix UI)
- **Language**: TypeScript - **Icons**: Lucide React
- **Markdown**: react-markdown
## Project Structure - **Language**: TypeScript
``` ## Project Structure
promptarch/
components/ ```
AIAssist.tsx # Main AI chat with plan-first workflow (1453 lines) promptarch/
PromptEnhancer.tsx # Prompt enhancement UI with intent detection (556 lines) components/
SettingsPanel.tsx # Provider configuration and API key management (569 lines) AIAssist.tsx # Main AI chat with plan-first workflow (1453 lines)
Sidebar.tsx # Navigation sidebar PromptEnhancer.tsx # Prompt enhancement UI with intent detection (556 lines)
GoogleAdsGenerator.tsx # Google Ads campaign generator SettingsPanel.tsx # Provider configuration and API key management (569 lines)
PRDGenerator.tsx # Product Requirements Document generator Sidebar.tsx # Navigation sidebar
ActionPlanGenerator.tsx # Action plan decomposition GoogleAdsGenerator.tsx # Google Ads campaign generator
SlidesGenerator.tsx # Presentation deck generator PRDGenerator.tsx # Product Requirements Document generator
MarketResearcher.tsx # Market research tool ActionPlanGenerator.tsx # Action plan decomposition
HistoryPanel.tsx # Chat history management SlidesGenerator.tsx # Presentation deck generator
lib/ MarketResearcher.tsx # Market research tool
enhance-engine.ts # Modular prompt enhancement (9 strategies) HistoryPanel.tsx # Chat history management
store.ts # Zustand state store lib/
artifact-utils.ts # Preview/rendering utilities enhance-engine.ts # Modular prompt enhancement (9 strategies)
export-utils.ts # Export to XLSX/HTML/ZIP store.ts # Zustand state store
services/ artifact-utils.ts # Preview/rendering utilities
qwen-oauth.ts # Qwen OAuth streaming service export-utils.ts # Export to XLSX/HTML/ZIP
ollama-cloud.ts # Ollama Cloud streaming service services/
zai-plan.ts # Z.AI Plan streaming service qwen-oauth.ts # Qwen OAuth streaming service
openrouter.ts # OpenRouter streaming service ollama-cloud.ts # Ollama Cloud streaming service
model-adapter.ts # Unified provider adapter zai-plan.ts # Z.AI Plan streaming service
adapter-instance.ts # Provider registry openrouter.ts # OpenRouter streaming service
i18n/ model-adapter.ts # Unified provider adapter
translations.ts # EN/RU/HE translations adapter-instance.ts # Provider registry
types/ i18n/
index.ts # TypeScript interfaces translations.ts # EN/RU/HE translations
``` types/
index.ts # TypeScript interfaces
## Versioning ```
This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). See [CHANGELOG.md](CHANGELOG.md) for detailed release notes. ## Versioning
| Version | Date | Highlights | This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). See [CHANGELOG.md](CHANGELOG.md) for detailed release notes.
|---------|------|------------|
| [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 | | Version | Date | Highlights |
| [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 | | [2.2.0](CHANGELOG.md#2.2.0---2026-03-19) | Leads Finder Agent Mode | 2026-03-19 |
| [1.1.0](CHANGELOG.md#110---2025-12-29) | 2025-12-29 17:55 | GitHub push, XLSX/HTML export, OAuth management | | [2.1.0](CHANGELOG.md#2.1.0---2026-03-18) | Full 17-Section Report, GEO Scoring, Action Plan, FAQ Gen | 2026-03-18 |
| [1.0.0](CHANGELOG.md#100---2025-12-29) | 2025-12-29 13:51 | Initial release | | [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 |
## Development | [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 |
```bash | [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 |
npm install # Install dependencies | [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 |
npm run dev # Development server (Turbopack) | [1.5.0](CHANGELOG.md#150---2026-03-18) | 2026-03-18 20:29 | Modification progress overlay, preview blink fix |
npm run build # Production build | [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 |
npm start # Start production server | [1.3.0](CHANGELOG.md#130---2026-03-18) | 2026-03-18 18:51 | Plan-first workflow, OpenRouter, post-coding UX, enhanced prompt engine |
npm run lint # Lint code | [1.2.0](CHANGELOG.md#120---2026-01-19) | 2026-01-19 19:16 | SEO agent fixes, Z.AI API validation |
``` | [1.1.0](CHANGELOG.md#110---2025-12-29) | 2025-12-29 17:55 | GitHub push, XLSX/HTML export, OAuth management |
| [1.0.0](CHANGELOG.md#100---2025-12-29) | 2025-12-29 13:51 | Initial release |
## Attribution & Credits
## Development
**Author**: Roman | RyzenAdvanced
- **Gitea**: [admin/PromptArch](https://github.rommark.dev/admin/PromptArch) ```bash
- **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem) npm install # Install dependencies
npm run dev # Development server (Turbopack)
**Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix) npm run build # Production build
- Visual and architectural evolution of the Clavix framework npm start # Start production server
npm run lint # Lint code
**Development Platform**: [TRAE.AI IDE](https://trae.ai) powered by [GLM 4.7](https://z.ai/subscribe?ic=R0K78RJKNW) ```
## License ## Attribution & Credits
ISC **Author**: Roman | RyzenAdvanced
- **Gitea**: [admin/PromptArch](https://github.rommark.dev/admin/PromptArch)
## Contributing - **Telegram**: [@VibeCodePrompterSystem](https://t.me/VibeCodePrompterSystem)
Contributions are welcome! Please feel free to submit a Pull Request. **Forked from**: [ClavixDev/Clavix](https://github.com/ClavixDev/Clavix)
- Visual and architectural evolution of the Clavix framework
**Development Platform**: [TRAE.AI IDE](https://trae.ai) powered by [GLM 4.7](https://z.ai/subscribe?ic=R0K78RJKNW)
## License
ISC
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

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,7 +702,121 @@ export default function AIAssist() {
setStatus(null); setStatus(null);
} }
const response = await modelAdapter.generateAIAssistStream( // 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(
{ {
messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }], messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }],
currentAgent, currentAgent,
@@ -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,7 +1470,82 @@ 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>
{msg.role === "assistant" && isProcessing && i === aiAssistHistory.length - 1 && status && ( {msg.role === "assistant" && isProcessing && i === aiAssistHistory.length - 1 && status && (
@@ -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",